diff --git a/.craft.yml b/.craft.yml index 37174e148af..00831009206 100644 --- a/.craft.yml +++ b/.craft.yml @@ -37,7 +37,6 @@ targets: maven:io.sentry:sentry-android-core: maven:io.sentry:sentry-android-ndk: maven:io.sentry:sentry-android-timber: - maven:io.sentry:sentry-android-okhttp: maven:io.sentry:sentry-kotlin-extensions: maven:io.sentry:sentry-android-fragment: maven:io.sentry:sentry-bom: @@ -45,9 +44,13 @@ targets: maven:io.sentry:sentry-opentelemetry-agent: maven:io.sentry:sentry-opentelemetry-agentcustomization: maven:io.sentry:sentry-opentelemetry-core: +# maven:io.sentry:sentry-opentelemetry-agentless: +# maven:io.sentry:sentry-opentelemetry-agentless-spring: maven:io.sentry:sentry-apollo: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: +# maven:io.sentry:sentry-graphql-core: +# maven:io.sentry:sentry-graphql-22: maven:io.sentry:sentry-quartz: maven:io.sentry:sentry-okhttp: maven:io.sentry:sentry-android-navigation: diff --git a/.github/ISSUE_TEMPLATE/bug_report_android.yml b/.github/ISSUE_TEMPLATE/bug_report_android.yml index 9b6bfc9ff6a..20db87e3631 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_android.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_android.yml @@ -10,13 +10,13 @@ body: options: - sentry-android - sentry-android-ndk - - sentry-android-okhttp - sentry-android-timber - sentry-android-fragment - sentry-android-sqlite - sentry-apollo - - sentry-compose - sentry-apollo-3 + - sentry-compose + - sentry-okhttp - other validations: required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index f802c3a0cc1..d71d61c074e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -15,6 +15,8 @@ body: - sentry-apollo-3 - sentry-kotlin-extensions - sentry-opentelemetry-agent + - sentry-opentelemetry-agentless + - sentry-opentelemetry-agentless-spring - sentry-opentelemetry-core - sentry-servlet - sentry-servlet-jakarta @@ -27,6 +29,7 @@ body: - sentry-logback - sentry-log4j2 - sentry-graphql + - sentry-graphql-22 - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 38ce5ab57ea..c4e60bcdabc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,8 +20,6 @@ jobs: strategy: fail-fast: false - matrix: - language: ['cpp', 'java'] steps: - name: Checkout Repo @@ -45,12 +43,7 @@ jobs: with: languages: ${{ matrix.language }} - - if: matrix.language == 'cpp' - name: Build Cpp - run: | - ./gradlew sentry-android-ndk:buildCMakeRelWithDebInfo - - if: matrix.language == 'java' - name: Build Java + - name: Build Java run: | ./gradlew buildForCodeQL diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index aa1c479d9d6..e8bb0d77e80 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -20,10 +20,26 @@ jobs: fail-fast: false matrix: sample: [ "sentry-samples-spring-boot-jakarta" ] + agent: [ "0" ] + agent-auto-init: [ "true" ] include: - sample: "sentry-samples-spring-boot" + - sample: "sentry-samples-spring-boot-opentelemetry-noagent" + - sample: "sentry-samples-spring-boot-opentelemetry" + agent: "1" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-opentelemetry" + agent: "1" + agent-auto-init: "false" - sample: "sentry-samples-spring-boot-webflux-jakarta" - sample: "sentry-samples-spring-boot-webflux" + - sample: "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" + - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" + agent: "1" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" + agent: "1" + agent-auto-init: "false" steps: - uses: actions/checkout@v4 with: @@ -53,7 +69,6 @@ jobs: -e '/.*"sentry-android-core",/d' \ -e '/.*"sentry-android-fragment",/d' \ -e '/.*"sentry-android-navigation",/d' \ - -e '/.*"sentry-android-okhttp",/d' \ -e '/.*"sentry-android-sqlite",/d' \ -e '/.*"sentry-android-timber",/d' \ -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \ @@ -78,20 +93,19 @@ jobs: run: | ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar + - name: Build agent jar + run: | + ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble + - name: Start server and run integration test for sentry-cli commands run: | - test/system-test-sentry-server-start.sh \ - > sentry-mock-server.txt 2>&1 & \ - test/system-test-spring-server-start.sh "${{ matrix.sample }}" \ - > spring-server.txt 2>&1 & \ - test/wait-for-spring.sh && \ - ./gradlew :sentry-samples:${{ matrix.sample }}:systemTest + test/system-test-run.sh "${{ matrix.sample }}" "${{ matrix.agent }}" "${{ matrix.agent-auto-init }}" - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: - name: test-results-${{ matrix.sample }}-system-test + name: test-results-${{ matrix.sample }}-${{ matrix.agent }}-${{ matrix.agent-auto-init }}-system-test path: | **/build/reports/* sentry-mock-server.txt diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 24fce64050f..83d90bb9199 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -13,7 +13,7 @@ jobs: native: uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 with: - path: sentry-android-ndk/sentry-native + path: scripts/update-sentry-native-ndk.sh name: Native SDK secrets: # If a custom token is used instead, a CI would be triggered on a created PR. diff --git a/.gitmodules b/.gitmodules index fe6c3b7cc09..e69de29bb2d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "sentry-android-ndk/sentry-native"] - path = sentry-android-ndk/sentry-native - url = https://github.com/getsentry/sentry-native diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c6c9c37558..5a20ae55f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Enable `ThreadLocalAccessor` for Spring Boot 3 WebFlux by default ([#4023](https://github.com/getsentry/sentry-java/pull/4023)) + ### Internal - Warm starts cleanup ([#3954](https://github.com/getsentry/sentry-java/pull/3954)) @@ -9,8 +13,402 @@ ### Dependencies - Bump Native SDK from v0.7.16 to v0.7.17 ([#4003](https://github.com/getsentry/sentry-java/pull/4003)) - - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0717) - - [diff](https://github.com/getsentry/sentry-native/compare/0.7.16...0.7.17) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0717) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.16...0.7.17) + +## 8.0.0-rc.3 + +### Features + +- Add `sentry-opentelemetry-agentless-spring` module ([#4000](https://github.com/getsentry/sentry-java/pull/4000)) + - This module can be added as a dependency when using Sentry with OpenTelemetry and Spring Boot but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry. + - You may want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use. +- Add `sentry-opentelemetry-agentless` module ([#3961](https://github.com/getsentry/sentry-java/pull/3961)) + - This module can be added as a dependency when using Sentry with OpenTelemetry but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry. + - To enable the auto configuration of it, please set `-Dotel.java.global-autoconfigure.enabled=true` on the `java` command, when starting your application. + - You may also want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use. +- `OpenTelemetryUtil.applyOpenTelemetryOptions` now takes an enum instead of a boolean for its mode +- Add `openTelemetryMode` option ([#3994](https://github.com/getsentry/sentry-java/pull/3994)) + - It defaults to `AUTO` meaning the SDK will figure out how to best configure itself for use with OpenTelemetry + - Use of OpenTelemetry can also be disabled completely by setting it to `OFF` ([#3995](https://github.com/getsentry/sentry-java/pull/3995)) + - In this case even if OpenTelemetry is present, the Sentry SDK will not use it + - Use `AGENT` when using `sentry-opentelemetry-agent` + - Use `AGENTLESS` when using `sentry-opentelemetry-agentless` + - Use `AGENTLESS_SPRING` when using `sentry-opentelemetry-agentless-spring` +- Add `scopeBindingMode` to `SpanOptions` ([#4004](https://github.com/getsentry/sentry-java/pull/4004)) + - This setting only affects the SDK when used with OpenTelemetry. + - Defaults to `AUTO` meaning the SDK will decide whether the span should be bound to the current scope. It will not bind transactions to scope using `AUTO`, it will only bind spans where the parent span is on the current scope. + - `ON` sets the new span on the current scope. + - `OFF` does not set the new span on the scope. + +### Fixes + +- Replace deprecated `SimpleInstrumentation` with `SimplePerformantInstrumentation` for graphql 22 ([#3974](https://github.com/getsentry/sentry-java/pull/3974)) +- Cache requests for Spring using Springs `ContentCachingRequestWrapper` instead of our own Wrapper to also cache parameters ([#3641](https://github.com/getsentry/sentry-java/pull/3641)) + - Previously only the body was cached which could lead to problems in the FilterChain as Request parameters were not available +- We now hold a strong reference to the underlying OpenTelemetry span when it is created through Sentry API ([#3997](https://github.com/getsentry/sentry-java/pull/3997)) + - This keeps it from being garbage collected too early +- Close backpressure monitor on SDK shutdown ([#3998](https://github.com/getsentry/sentry-java/pull/3998)) + - Due to the backpressure monitor rescheduling a task to run every 10s, it very likely caused shutdown to wait the full `shutdownTimeoutMillis` (defaulting to 2s) instead of being able to terminate immediately +- Improve ignored check performance ([#3992](https://github.com/getsentry/sentry-java/pull/3992)) + - Checking if a span origin, a transaction or a checkIn should be ignored is now faster + +## 8.0.0-rc.2 + +### Fixes + +- Fix incoming defer sampling decision `sentry-trace` header ([#3942](https://github.com/getsentry/sentry-java/pull/3942)) + - A `sentry-trace` header that only contains trace ID and span ID but no sampled flag (`-1`, `-0` suffix) means the receiving system can make its own sampling decision + - When generating `sentry-trace` header from `PropagationContext` we now copy the `sampled` flag. + - In `TransactionContext.fromPropagationContext` when there is no parent sampling decision, keep the decision `null` so a new sampling decision is made instead of defaulting to `false` +- Defer sampling decision by setting `sampled` to `null` in `PropagationContext` when using OpenTelemetry in case of an incoming defer sampling `sentry-trace` header. ([#3945](https://github.com/getsentry/sentry-java/pull/3945)) +- Build `PropagationContext` from `SamplingDecision` made by `SentrySampler` instead of parsing headers and potentially ignoring a sampling decision in case a `sentry-trace` header comes in with deferred sampling decision. ([#3947](https://github.com/getsentry/sentry-java/pull/3947)) +- Let OpenTelemetry handle extracting and injecting tracing information ([#3953](https://github.com/getsentry/sentry-java/pull/3953)) + - Our integrations no longer call `.continueTrace` and also do not inject tracing headers if the integration has been added to `ignoredSpanOrigins` + +## 8.0.0-rc.1 + +### Features + +- Extract OpenTelemetry `URL_PATH` span attribute into description ([#3933](https://github.com/getsentry/sentry-java/pull/3933)) +- Replace OpenTelemetry `ContextStorage` wrapper with `ContextStorageProvider` ([#3938](https://github.com/getsentry/sentry-java/pull/3938)) + - The wrapper had to be put in place before any call to `Context` whereas `ContextStorageProvider` is automatically invoked at the correct time. + +### Dependencies + +- Bump OpenTelemetry to 1.44.1, OpenTelemetry Java Agent to 2.10.0 and Semantic Conventions to 1.28.0 ([#3935](https://github.com/getsentry/sentry-java/pull/3935)) + +### Fixes + +- Fix testTag not working for Jetpack Compose user interaction tracking ([#3878](https://github.com/getsentry/sentry-java/pull/3878)) + +## 8.0.0-beta.3 + +### Features + +- Send `otel.kind` to Sentry ([#3907](https://github.com/getsentry/sentry-java/pull/3907)) +- Allow passing `environment` to `CheckinUtils.withCheckIn` ([3889](https://github.com/getsentry/sentry-java/pull/3889)) +- Changes up to `7.18.0` have been merged and are now included as well + +### Fixes + +- Mark `DiskFlushNotification` hint flushed when rate limited ([#3892](https://github.com/getsentry/sentry-java/pull/3892)) + - Our `UncaughtExceptionHandlerIntegration` waited for the full flush timeout duration (default 15s) when rate limited. +- Do not replace `op` with auto generated content for OpenTelemetry spans with span kind `INTERNAL` ([#3906](https://github.com/getsentry/sentry-java/pull/3906)) + +### Behavioural Changes + +- Send file name and path only if isSendDefaultPii is true ([#3919](https://github.com/getsentry/sentry-java/pull/3919)) + +## 8.0.0-beta.2 + +### Breaking Changes + +- Use String instead of UUID for SessionId ([#3834](https://github.com/getsentry/sentry-java/pull/3834)) + - The `Session` constructor now takes a `String` instead of a `UUID` for the `sessionId` parameter. + - `Session.getSessionId()` now returns a `String` instead of a `UUID`. +- The Android minSdk level for all Android modules is now 21 ([#3852](https://github.com/getsentry/sentry-java/pull/3852)) +- The minSdk level for sentry-android-ndk changed from 19 to 21 ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) +- All status codes below 400 are now mapped to `SpanStatus.OK` ([#3869](https://github.com/getsentry/sentry-java/pull/3869)) + +### Features + +- Spring Boot now automatically detects if OpenTelemetry is available and makes use of it ([#3846](https://github.com/getsentry/sentry-java/pull/3846)) + - This is only enabled if there is no OpenTelemetry agent available + - We prefer to use the OpenTelemetry agent as it offers more auto instrumentation + - In some cases the OpenTelemetry agent cannot be used, please see https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/ for more details on when to prefer the Agent and when the Spring Boot starter makes more sense. + - In this mode the SDK makes use of the `OpenTelemetry` bean that is created by `opentelemetry-spring-boot-starter` instead of `GlobalOpenTelemetry` +- Spring Boot now automatically detects our OpenTelemetry agent if its auto init is disabled ([#3848](https://github.com/getsentry/sentry-java/pull/3848)) + - This means Spring Boot config mechanisms can now be combined with our OpenTelemetry agent + - The `sentry-opentelemetry-extra` module has been removed again, most classes have been moved to `sentry-opentelemetry-bootstrap` which is loaded into the bootstrap classloader (i.e. `null`) when our Java agent is used. The rest has been moved into `sentry-opentelemetry-agentcustomization` and is loaded into the agent classloader when our Java agent is used. + - The `sentry-opentelemetry-bootstrap` and `sentry-opentelemetry-agentcustomization` modules can be used without the agent as well, in which case all classes are loaded into the application classloader. Check out our `sentry-samples-spring-boot-jakarta-opentelemetry-noagent` sample. + - In this mode the SDK makes use of `GlobalOpenTelemetry` +- Automatically set span factory based on presence of OpenTelemetry ([#3858](https://github.com/getsentry/sentry-java/pull/3858)) + - `SentrySpanFactoryHolder` has been removed as it is no longer required. +- Add `ignoredTransactions` option to filter out transactions by name ([#3871](https://github.com/getsentry/sentry-java/pull/3871)) + - can be used via ENV vars, e.g. `SENTRY_IGNORED_TRANSACTIONS=POST /person/,GET /pers.*` + - can also be set in options directly, e.g. `options.setIgnoredTransactions(...)` + - can also be set in `sentry.properties`, e.g. `ignored-transactions=POST /person/,GET /pers.*` + - can also be set in Spring config `application.properties`, e.g. `sentry.ignored-transactions=POST /person/,GET /pers.*` +- Add a sample for showcasing Sentry with OpenTelemetry for Spring Boot 3 with our Java agent (`sentry-samples-spring-boot-jakarta-opentelemetry`) ([#3856](https://github.com/getsentry/sentry-java/pull/3828)) +- Add a sample for showcasing Sentry with OpenTelemetry for Spring Boot 3 without our Java agent (`sentry-samples-spring-boot-jakarta-opentelemetry-noagent`) ([#3856](https://github.com/getsentry/sentry-java/pull/3856)) +- Add a sample for showcasing Sentry with OpenTelemetry (`sentry-samples-console-opentelemetry-noagent`) ([#3856](https://github.com/getsentry/sentry-java/pull/3862)) +- Add `globalHubMode` to options ([#3805](https://github.com/getsentry/sentry-java/pull/3805)) + - `globalHubMode` used to only be a param on `Sentry.init`. To make it easier to be used in e.g. Desktop environments, we now additionally added it as an option on SentryOptions that can also be set via `sentry.properties`. + - If both the param on `Sentry.init` and the option are set, the option will win. By default the option is set to `null` meaning whatever is passed to `Sentry.init` takes effect. +- Lazy uuid generation for SentryId and SpanId ([#3770](https://github.com/getsentry/sentry-java/pull/3770)) +- Faster generation of Sentry and Span IDs ([#3818](https://github.com/getsentry/sentry-java/pull/3818)) + - Uses faster implementation to convert UUID to SentryID String + - Uses faster Random implementation to generate UUIDs +- Android 15: Add support for 16KB page sizes ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) + - See https://developer.android.com/guide/practices/page-sizes for more details +- Changes up to `7.17.0` have been merged and are now included as well + +### Fixes + +- The Sentry OpenTelemetry Java agent now makes sure Sentry `Scopes` storage is initialized even if the agents auto init is disabled ([#3848](https://github.com/getsentry/sentry-java/pull/3848)) + - This is required for all integrations to work together with our OpenTelemetry Java agent if its auto init has been disabled and the SDKs init should be used instead. +- Do not ignore certain span origins for OpenTelemetry without agent ([#3856](https://github.com/getsentry/sentry-java/pull/3856)) +- Fix `startChild` for span that is not in current OpenTelemetry `Context` ([#3862](https://github.com/getsentry/sentry-java/pull/3862)) + - Starting a child span from a transaction that wasn't in the current `Context` lead to multiple transactions being created (one for the transaction and another per span created). +- Add `auto.graphql.graphql22` to ignored span origins when using OpenTelemetry ([#3828](https://github.com/getsentry/sentry-java/pull/3828)) +- The Spring Boot 3 WebFlux sample now uses our GraphQL v22 integration ([#3828](https://github.com/getsentry/sentry-java/pull/3828)) +- All status codes below 400 are now mapped to `SpanStatus.OK` ([#3869](https://github.com/getsentry/sentry-java/pull/3869)) + + +### Dependencies + +- Bump Native SDK from v0.7.5 to v0.7.14 ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) ([#3914](https://github.com/getsentry/sentry-java/pull/3914)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0714) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.5...0.7.14) + +### Behavioural Changes + +- (Android) Enable Performance V2 by default ([#3824](https://github.com/getsentry/sentry-java/pull/3824)) + - With this change cold app start spans will include spans for ContentProviders, Application and Activity load. + +## 8.0.0-beta.1 + +### Breaking Changes + +- Throw IllegalArgumentException when calling Sentry.init on Android ([#3596](https://github.com/getsentry/sentry-java/pull/3596)) +- Metrics have been removed from the SDK ([#3774](https://github.com/getsentry/sentry-java/pull/3774)) + - Metrics will return but we don't know in what exact form yet +- `enableTracing` option (a.k.a `enable-tracing`) has been removed from the SDK ([#3776](https://github.com/getsentry/sentry-java/pull/3776)) + - Please set `tracesSampleRate` to a value >= 0.0 for enabling performance instead. The default value is `null` which means performance is disabled. +- Change OkHttp sub-spans to span attributes ([#3556](https://github.com/getsentry/sentry-java/pull/3556)) + - This will reduce the number of spans created by the SDK +- Replace `synchronized` methods and blocks with `ReentrantLock` (`AutoClosableReentrantLock`) ([#3715](https://github.com/getsentry/sentry-java/pull/3715)) + - If you are subclassing any Sentry classes, please check if the parent class used `synchronized` before. Please make sure to use the same lock object as the parent class in that case. +- `traceOrigins` option (`io.sentry.traces.tracing-origins` in manifest) has been removed, please use `tracePropagationTargets` (`io.sentry.traces.trace-propagation-targets` in manifest`) instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `profilingEnabled` option (`io.sentry.traces.profiling.enable` in manifest) has been removed, please use `profilesSampleRate` (`io.sentry.traces.profiling.sample-rate` instead) instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `shutdownTimeout` option has been removed, please use `shutdownTimeoutMillis` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `profilingTracesIntervalMillis` option for Android has been removed ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `io.sentry.session-tracking.enable` manifest option has been removed ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `Sentry.traceHeaders()` method has been removed, please use `Sentry.getTraceparent()` instead ([#3718](https://github.com/getsentry/sentry-java/pull/3718)) +- `Sentry.reportFullDisplayed()` method has been removed, please use `Sentry.reportFullyDisplayed()` instead ([#3717](https://github.com/getsentry/sentry-java/pull/3717)) +- `User.other` has been removed, please use `data` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `SdkVersion.getIntegrations()` has been removed, please use `getIntegrationSet` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `SdkVersion.getPackages()` has been removed, please use `getPackageSet()` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `Device.language` has been removed, please use `locale` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `TraceContext.user` and `TraceContextUser` class have been removed, please use `userId` on `TraceContext` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `TransactionContext.fromSentryTrace()` has been removed, please use `Sentry.continueTrace()` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- `SentryDataFetcherExceptionHandler` has been removed, please use `SentryGenericDataFetcherExceptionHandler` in combination with `SentryInstrumentation` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) +- One of the `AndroidTransactionProfiler` constructors has been removed, please use a different one ([#3780](https://github.com/getsentry/sentry-java/pull/3780)) + +### Features + +- Add init priority settings ([#3674](https://github.com/getsentry/sentry-java/pull/3674)) + - You may now set `forceInit=true` (`force-init` for `.properties` files) to ensure a call to Sentry.init / SentryAndroid.init takes effect +- Add force init option to Android Manifest ([#3675](https://github.com/getsentry/sentry-java/pull/3675)) + - Use `` to ensure Sentry Android auto init is not easily overwritten +- Attach request body for `application/x-www-form-urlencoded` requests in Spring ([#3731](https://github.com/getsentry/sentry-java/pull/3731)) + - Previously request body was only attached for `application/json` requests +- Set breadcrumb level based on http status ([#3771](https://github.com/getsentry/sentry-java/pull/3771)) +- Support `graphql-java` v22 via a new module `sentry-graphql-22` ([#3740](https://github.com/getsentry/sentry-java/pull/3740)) + - If you are using `graphql-java` v21 or earlier, you can use the `sentry-graphql` module + - For `graphql-java` v22 and newer please use the `sentry-graphql-22` module +- We now provide a `SentryInstrumenter` bean directly for Spring (Boot) if there is none yet instead of using `GraphQlSourceBuilderCustomizer` to add the instrumentation ([#3744](https://github.com/getsentry/sentry-java/pull/3744)) + - It is now also possible to provide a bean of type `SentryGraphqlInstrumentation.BeforeSpanCallback` which is then used by `SentryInstrumenter` +- Emit transaction.data inside contexts.trace.data ([#3735](https://github.com/getsentry/sentry-java/pull/3735)) + - Also does not emit `transaction.data` in `exras` anymore + +### Fixes + +- Use OpenTelemetry span name as fallback for transaction name ([#3557](https://github.com/getsentry/sentry-java/pull/3557)) + - In certain cases we were sending transactions as "" when using OpenTelemetry +- Add OpenTelemetry span data to Sentry span ([#3593](https://github.com/getsentry/sentry-java/pull/3593)) +- No longer selectively copy OpenTelemetry attributes to Sentry spans / transactions `data` ([#3663](https://github.com/getsentry/sentry-java/pull/3663)) +- Remove `PROCESS_COMMAND_ARGS` (`process.command_args`) OpenTelemetry span attribute as it can be very large ([#3664](https://github.com/getsentry/sentry-java/pull/3664)) +- Use RECORD_ONLY sampling decision if performance is disabled ([#3659](https://github.com/getsentry/sentry-java/pull/3659)) + - Also fix check whether Performance is enabled when making a sampling decision in the OpenTelemetry sampler +- Sentry OpenTelemetry Java Agent now sets Instrumenter to SENTRY (used to be OTEL) ([#3697](https://github.com/getsentry/sentry-java/pull/3697)) +- Set span origin in `ActivityLifecycleIntegration` on span options instead of after creating the span / transaction ([#3702](https://github.com/getsentry/sentry-java/pull/3702)) + - This allows spans to be filtered by span origin on creation +- Honor ignored span origins in `SentryTracer.startChild` ([#3704](https://github.com/getsentry/sentry-java/pull/3704)) +- Add `enable-spotlight` and `spotlight-connection-url` to external options and check if spotlight is enabled when deciding whether to inspect an OpenTelemetry span for connecting to splotlight ([#3709](https://github.com/getsentry/sentry-java/pull/3709)) +- Trace context on `Contexts.setTrace` has been marked `@NotNull` ([#3721](https://github.com/getsentry/sentry-java/pull/3721)) + - Setting it to `null` would cause an exception. + - Transactions are dropped if trace context is missing +- Remove internal annotation on `SpanOptions` ([#3722](https://github.com/getsentry/sentry-java/pull/3722)) +- `SentryLogbackInitializer` is now public ([#3723](https://github.com/getsentry/sentry-java/pull/3723)) +- Fix order of calling `close` on previous Sentry instance when re-initializing ([#3750](https://github.com/getsentry/sentry-java/pull/3750)) + - Previously some parts of Sentry were immediately closed after re-init that should have stayed open and some parts of the previous init were never closed + +### Behavioural Changes + +- (Android) Replace thread id with kernel thread id in span data ([#3706](https://github.com/getsentry/sentry-java/pull/3706)) + +### Dependencies + +- Bump OpenTelemetry to 1.41.0, OpenTelemetry Java Agent to 2.7.0 and Semantic Conventions to 1.25.0 ([#3668](https://github.com/getsentry/sentry-java/pull/3668)) + +## 8.0.0-alpha.4 + +### Fixes + +- Removed user segment ([#3512](https://github.com/getsentry/sentry-java/pull/3512)) +- Use span id of remote parent ([#3548](https://github.com/getsentry/sentry-java/pull/3548)) + - Traces were broken because on an incoming request, OtelSentrySpanProcessor did not set the parentSpanId on the span correctly. Traces were not referencing the actual parent span but some other (random) span ID which the server doesn't know. +- Attach active span to scope when using OpenTelemetry ([#3549](https://github.com/getsentry/sentry-java/pull/3549)) + - Errors weren't linked to traces correctly due to parts of the SDK not knowing the current span +- Record dropped spans in client report when sampling out OpenTelemetry spans ([#3552](https://github.com/getsentry/sentry-java/pull/3552)) +- Retrieve the correct current span from `Scope`/`Scopes` when using OpenTelemetry ([#3554](https://github.com/getsentry/sentry-java/pull/3554)) + +## 8.0.0-alpha.3 + +### Breaking Changes + +- `sentry-android-okhttp` has been removed in favor of `sentry-okhttp`, removing android dependency from the module ([#3510](https://github.com/getsentry/sentry-java/pull/3510)) + +### Fixes + +- Support spans that are split into multiple batches ([#3539](https://github.com/getsentry/sentry-java/pull/3539)) + - When spans belonging to a single transaction were split into multiple batches for SpanExporter, we did not add all spans because the isSpanTooOld check wasn't inverted. +- Parse and use `send-default-pii` and `max-request-body-size` from `sentry.properties` ([#3534](https://github.com/getsentry/sentry-java/pull/3534)) +- `span.startChild` now uses `.makeCurrent()` by default ([#3544](https://github.com/getsentry/sentry-java/pull/3544)) + - This caused an issue where the span tree wasn't correct because some spans were not added to their direct parent +- Partially fix bootstrap class loading ([#3543](https://github.com/getsentry/sentry-java/pull/3543)) + - There was a problem with two separate Sentry `Scopes` being active inside each OpenTelemetry `Context` due to using context keys from more than one class loader. + +## 8.0.0-alpha.2 + +### Behavioural Changes + +- (Android) The JNI layer for sentry-native has now been moved from sentry-java to sentry-native ([#3189](https://github.com/getsentry/sentry-java/pull/3189)) + - This now includes prefab support for sentry-native, allowing you to link and access the sentry-native API within your native app code + - Checkout the `sentry-samples/sentry-samples-android` example on how to configure CMake and consume `sentry.h` + +### Features + +- Our `sentry-opentelemetry-agent` has been completely reworked and now plays nicely with the rest of the Java SDK + - You may also want to give this new agent a try even if you haven't used OpenTelemetry (with Sentry) before. It offers support for [many more libraries and frameworks](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md), improving on our trace propagation, `Scopes` (used to be `Hub`) propagation as well as performance instrumentation (i.e. more spans). + - If you are using a framework we did not support before and currently resort to manual instrumentation, please give the agent a try. See [here for a list of supported libraries, frameworks and application servers](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md). + - NOTE: Not all features have been implemented yet for the OpenTelemetry agent. Features of note that are not working yet: + - Metrics + - Measurements + - `forceFinish` on transaction + - `scheduleFinish` on transaction + - see [#3436](https://github.com/getsentry/sentry-java/issues/3436) for a more up-to-date list of features we have (not) implemented + - Please see "Installing `sentry-opentelemetry-agent`" for more details on how to set up the agent. + - What's new about the Agent + - When the OpenTelemetry Agent is used, Sentry API creates OpenTelemetry spans under the hood, handing back a wrapper object which bridges the gap between traditional Sentry API and OpenTelemetry. We might be replacing some of the Sentry performance API in the future. + - This is achieved by configuring the SDK to use `OtelSpanFactory` instead of `DefaultSpanFactory` which is done automatically by the auto init of the Java Agent. + - OpenTelemetry spans are now only turned into Sentry spans when they are finished so they can be sent to the Sentry server. + - Now registers an OpenTelemetry `Sampler` which uses Sentry sampling configuration + - Other Performance integrations automatically stop creating spans to avoid duplicate spans + - The Sentry SDK now makes use of OpenTelemetry `Context` for storing Sentry `Scopes` (which is similar to what used to be called `Hub`) and thus relies on OpenTelemetry for `Context` propagation. + - Classes used for the previous version of our OpenTelemetry support have been deprecated but can still be used manually. We're not planning to keep the old agent around in favor of less complexity in the SDK. +- Add `ignoredSpanOrigins` option for ignoring spans coming from certain integrations + - We pre-configure this to ignore Performance instrumentation for Spring and other integrations when using our OpenTelemetry Agent to avoid duplicate spans +- Add data fetching environment hint to breadcrumb for GraphQL (#3413) ([#3431](https://github.com/getsentry/sentry-java/pull/3431)) + +### Fixes + +- `TracesSampler` is now only created once in `SentryOptions` instead of creating a new one for every `Hub` (which is now `Scopes`). This means we're now creating fewer `SecureRandom` instances. +- Move onFinishCallback before span or transaction is finished ([#3459](https://github.com/getsentry/sentry-java/pull/3459)) +- Add timestamp when a profile starts ([#3442](https://github.com/getsentry/sentry-java/pull/3442)) +- Move fragment auto span finish to onFragmentStarted ([#3424](https://github.com/getsentry/sentry-java/pull/3424)) +- Remove profiling timeout logic and disable profiling on API 21 ([#3478](https://github.com/getsentry/sentry-java/pull/3478)) +- Properly reset metric flush flag on metric emission ([#3493](https://github.com/getsentry/sentry-java/pull/3493)) + +### Migration Guide / Deprecations + +- Classes used for the previous version of the Sentry OpenTelemetry Java Agent have been deprecated (`SentrySpanProcessor`, `SentryPropagator`, `OpenTelemetryLinkErrorEventProcessor`) +- Sentry OpenTelemetry Java Agent has been reworked and now allows you to manually create spans using Sentry API as well. +- Please see "Installing `sentry-opentelemetry-agent`" for more details on how to set up the agent. + +### Installing `sentry-opentelemetry-agent` + +#### Upgrading from a previous agent +If you've been using the previous version of `sentry-opentelemetry-agent`, simply replace the agent JAR with the [latest release](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-agent?smo=true) and start your application. That should be it. + +#### New to the agent +If you've not been using OpenTelemetry before, you can add `sentry-opentelemetry-agent` to your setup by downloading the latest release and using it when starting up your application +- `SENTRY_PROPERTIES_FILE=sentry.properties java -javaagent:sentry-opentelemetry-agent-x.x.x.jar -jar your-application.jar` +- Please use `sentry.properties` or environment variables to configure the SDK as the agent is now in charge of initializing the SDK and options coming from things like logging integrations or our Spring Boot integration will not take effect. +- You may find the [docs page](https://docs.sentry.io/platforms/java/tracing/instrumentation/opentelemetry/#using-sentry-opentelemetry-agent-with-auto-initialization) useful. While we haven't updated it yet to reflect the changes described here, the section about using the agent with auto init should still be valid. + +If you want to skip auto initialization of the SDK performed by the agent, please follow the steps above and set the environment variable `SENTRY_AUTO_INIT` to `false` then add the following to your `Sentry.init`: + +``` +Sentry.init(options -> { + options.setDsn("https://3d2ac63d6e1a4c6e9214443678f119a3@o87286.ingest.us.sentry.io/1801383"); + OpenTelemetryUtil.applyOpenTelemetryOptions(options); + ... +}); +``` + +If you're using our Spring (Boot) integration with auto init, use the following: +``` +@Bean +Sentry.OptionsConfiguration optionsConfiguration() { + return (options) -> { + OpenTelemetryUtil.applyOpenTelemetryOptions(options); + }; +} +``` + +### Dependencies + +- Bump Native SDK from v0.7.0 to v0.7.5 ([#3441](https://github.com/getsentry/sentry-java/pull/3189)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#075) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.0...0.7.5) + +## 8.0.0-alpha.1 + +Version 8 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: + +- New `Scope` types have been introduced, see "Behavioural Changes" for more details. +- Lifecycle tokens have been introduced to manage `Scope` lifecycle, see "Behavioural Changes" for more details. +- `Hub` has been replaced by `Scopes` + +### Behavioural Changes + +- We're introducing some new `Scope` types in the SDK, allowing for better control over what data is attached where. Previously there was a stack of scopes that was pushed and popped. Instead we now fork scopes for a given lifecycle and then restore the previous scopes. Since `Hub` is gone, it is also never cloned anymore. Separation of data now happens through the different scope types while making it easier to manipulate exactly what you need without having to attach data at the right time to have it apply where wanted. + - Global scope is attached to all events created by the SDK. It can also be modified before `Sentry.init` has been called. It can be manipulated using `Sentry.configureScope(ScopeType.GLOBAL, (scope) -> { ... })`. + - Isolation scope can be used e.g. to attach data to all events that come up while handling an incoming request. It can also be used for other isolation purposes. It can be manipulated using `Sentry.configureScope(ScopeType.ISOLATION, (scope) -> { ... })`. The SDK automatically forks isolation scope in certain cases like incoming requests, CRON jobs, Spring `@Async` and more. + - Current scope is forked often and data added to it is only added to events that are created while this scope is active. Data is also passed on to newly forked child scopes but not to parents. +- `Sentry.popScope` has been deprecated, please call `.close()` on the token returned by `Sentry.pushScope` instead or use it in a way described in more detail in "Migration Guide". +- We have chosen a default scope that is used for `Sentry.configureScope()` as well as API like `Sentry.setTag()` + - For Android the type defaults to `CURRENT` scope + - For Backend and other JVM applicatons it defaults to `ISOLATION` scope +- Event processors on `Scope` can now be ordered by overriding the `getOrder` method on implementations of `EventProcessor`. NOTE: This order only applies to event processors on `Scope` but not `SentryOptions` at the moment. Feel free to request this if you need it. +- `Hub` is deprecated in favor of `Scopes`, alongside some `Hub` relevant APIs. More details can be found in the "Migration Guide" section. + +### Breaking Changes + +- `Contexts` no longer extends `ConcurrentHashMap`, instead we offer a selected set of methods. + +### Migration Guide / Deprecations + +- `Hub` has been deprecated, we're replacing the following: + - `IHub` has been replaced by `IScopes`, however you should be able to simply pass `IHub` instances to code expecting `IScopes`, allowing for an easier migration. + - `HubAdapter.getInstance()` has been replaced by `ScopesAdapter.getInstance()` + - The `.clone()` method on `IHub`/`IScopes` has been deprecated, please use `.pushScope()` or `.pushIsolationScope()` instead + - Some internal methods like `.getCurrentHub()` and `.setCurrentHub()` have also been replaced. +- `Sentry.popScope` has been replaced by calling `.close()` on the token returned by `Sentry.pushScope()` and `Sentry.pushIsolationScope()`. The token can also be used in a `try` block like this: + +``` +try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushScope()) { + // this block has its separate current scope +} +``` + +as well as: + + +``` +try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushIsolationScope()) { + // this block has its separate isolation scope +} +``` + +You may also use `LifecycleHelper.close(token)`, e.g. in case you need to pass the token around for closing later. + +### Features + +- Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396) ## 7.20.0 @@ -70,16 +468,16 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE ### Fixes - Session Replay: fix various crashes and issues ([#3970](https://github.com/getsentry/sentry-java/pull/3970)) - - Fix `IndexOutOfBoundsException` when tracking window changes - - Fix `IllegalStateException` when adding/removing draw listener for a dead view - - Fix `ConcurrentModificationException` when registering window listeners and stopping `WindowRecorder`/`GestureRecorder` + - Fix `IndexOutOfBoundsException` when tracking window changes + - Fix `IllegalStateException` when adding/removing draw listener for a dead view + - Fix `ConcurrentModificationException` when registering window listeners and stopping `WindowRecorder`/`GestureRecorder` - Add support for setting sentry-native handler_strategy ([#3671](https://github.com/getsentry/sentry-java/pull/3671)) ### Dependencies - Bump Native SDK from v0.7.8 to v0.7.16 ([#3671](https://github.com/getsentry/sentry-java/pull/3671)) - - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0716) - - [diff](https://github.com/getsentry/sentry-native/compare/0.7.8...0.7.16) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0716) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.8...0.7.16) ## 7.18.1 @@ -92,7 +490,7 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE ### Features - Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) - - See https://developer.android.com/guide/practices/page-sizes for more details + - See https://developer.android.com/guide/practices/page-sizes for more details - Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855)) - Session Replay: Add support for masking/unmasking view containers ([#3881](https://github.com/getsentry/sentry-java/pull/3881)) @@ -101,14 +499,14 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE - Avoid collecting normal frames ([#3782](https://github.com/getsentry/sentry-java/pull/3782)) - Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) - Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888)) - - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported + - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported - Session Replay: Disable replay in session mode when rate limit is active ([#3854](https://github.com/getsentry/sentry-java/pull/3854)) ### Dependencies - Bump Native SDK from v0.7.2 to v0.7.8 ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) - - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078) - - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8) ## 7.17.0 @@ -122,8 +520,8 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE - Using MaxBreadcrumb with value 0 no longer crashes. ([#3836](https://github.com/getsentry/sentry-java/pull/3836)) - Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823)) - Fix standalone tomcat jndi issue ([#3873](https://github.com/getsentry/sentry-java/pull/3873)) - - Using Sentry Spring Boot on a standalone tomcat caused the following error: - - Failed to bind properties under 'sentry.parsed-dsn' to io.sentry.Dsn + - Using Sentry Spring Boot on a standalone tomcat caused the following error: + - Failed to bind properties under 'sentry.parsed-dsn' to io.sentry.Dsn ## 7.16.0 diff --git a/README.md b/README.md index c60d10ca327..50c281cdfaa 100644 --- a/README.md +++ b/README.md @@ -18,25 +18,24 @@ Sentry SDK for Java and Android | Packages | Maven Central | Minimum Android API Version | |-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | -| sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 19 | -| sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 19 | -| sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 19 | -| sentry-android-okhttp | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp) | 21 | -| sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 19 | -| sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 19 | -| sentry-android-navigation | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 19 | -| sentry-android-sqlite | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite) | 19 | +| sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 21 | +| sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 21 | +| sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 21 | +| sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 21 | +| sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 21 | +| sentry-android-navigation | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 21 | +| sentry-android-sqlite | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite) | 21 | | sentry-android-replay | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-replay/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-replay) | 26 | | sentry-compose-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android) | 21 | | sentry-compose-desktop | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop) | | sentry-compose | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose) | | sentry-apache-http-client-5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5) | -| sentry | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 19 | +| sentry | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 21 | | sentry-jul | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul) | | sentry-jdbc | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc) | -| sentry-apollo | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 19 | -| sentry-apollo-3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3) | 19 | -| sentry-kotlin-extensions | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 19 | +| sentry-apollo | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 21 | +| sentry-apollo-3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3) | 21 | +| sentry-kotlin-extensions | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 21 | | sentry-servlet | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet) | | | sentry-servlet-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta) | | | sentry-spring-boot | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot) | @@ -49,6 +48,8 @@ Sentry SDK for Java and Android | sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | | sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | | sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | +| sentry-graphql-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core) | +| sentry-graphql-22 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22) | | sentry-quartz | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) | | sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | | sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) | diff --git a/build.gradle.kts b/build.gradle.kts index 86cd98d54ad..e7d828e61f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,10 +32,6 @@ buildscript { classpath(Config.QualityPlugins.errorpronePlugin) classpath(Config.QualityPlugins.gradleVersionsPlugin) - // add classpath of androidNativeBundle - // com.ydq.android.gradle.build.tool:nativeBundle:{version}} - classpath(Config.NativePlugins.nativeBundlePlugin) - // add classpath of sentry android gradle plugin // classpath("io.sentry:sentry-android-gradle-plugin:{version}") @@ -55,6 +51,7 @@ apiValidation { listOf( "sentry-samples-android", "sentry-samples-console", + "sentry-samples-console-opentelemetry-noagent", "sentry-samples-jul", "sentry-samples-log4j2", "sentry-samples-logback", @@ -63,7 +60,11 @@ apiValidation { "sentry-samples-spring", "sentry-samples-spring-jakarta", "sentry-samples-spring-boot", + "sentry-samples-spring-boot-opentelemetry", + "sentry-samples-spring-boot-opentelemetry-noagent", "sentry-samples-spring-boot-jakarta", + "sentry-samples-spring-boot-jakarta-opentelemetry", + "sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "sentry-samples-spring-boot-webflux", "sentry-samples-spring-boot-webflux-jakarta", "sentry-uitest-android", @@ -80,6 +81,7 @@ allprojects { repositories { google() mavenCentral() + mavenLocal() } group = Config.Sentry.group version = properties[Config.Sentry.versionNameProp].toString() @@ -101,7 +103,7 @@ allprojects { dependsOn("cleanTest") } withType { - options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Werror", "-Xlint:-classfile", "-Xlint:-processing")) + options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Werror", "-Xlint:-classfile", "-Xlint:-processing", "-Xlint:-try")) } } } @@ -112,7 +114,6 @@ subprojects { "sentry-android-fragment", "sentry-android-navigation", "sentry-android-ndk", - "sentry-android-okhttp", "sentry-android-sqlite", "sentry-android-replay", "sentry-android-timber" @@ -297,7 +298,6 @@ private val androidLibs = setOf( "sentry-android-ndk", "sentry-android-fragment", "sentry-android-navigation", - "sentry-android-okhttp", "sentry-android-timber", "sentry-compose-android", "sentry-android-sqlite", diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index bf5dba03be5..c52cfdc642c 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -7,7 +7,7 @@ object Config { val kotlinStdLib = "stdlib-jdk8" val springBootVersion = "2.7.5" - val springBoot3Version = "3.3.2" + val springBoot3Version = "3.4.0" val kotlinCompatibleLanguageVersion = "1.4" val composeVersion = "1.5.3" @@ -33,11 +33,7 @@ object Config { object Android { private val sdkVersion = 34 - val minSdkVersion = 19 - val minSdkVersionOkHttp = 21 - val minSdkVersionReplay = 19 - val minSdkVersionNdk = 19 - val minSdkVersionCompose = 21 + val minSdkVersion = 21 val targetSdkVersion = sdkVersion val compileSdkVersion = sdkVersion @@ -98,6 +94,7 @@ object Config { val springBoot3StarterSecurity = "org.springframework.boot:spring-boot-starter-security:$springBoot3Version" val springBoot3StarterJdbc = "org.springframework.boot:spring-boot-starter-jdbc:$springBoot3Version" val springBoot3StarterActuator = "org.springframework.boot:spring-boot-starter-actuator:$springBoot3Version" + val springBoot3StarterOpenTelemetry = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:${OpenTelemetry.otelInstrumentationVersion}" val springWeb = "org.springframework:spring-webmvc" val springWebflux = "org.springframework:spring-webflux" @@ -133,6 +130,7 @@ object Config { val p6spy = "p6spy:p6spy:3.9.1" val graphQlJava = "com.graphql-java:graphql-java:17.3" + val graphQlJava22 = "com.graphql-java:graphql-java:22.1" val quartz = "org.quartz-scheduler:quartz:2.3.0" @@ -151,23 +149,28 @@ object Config { val composeUiReplay = "androidx.compose.ui:ui:1.5.0" // Note: don't change without testing forwards compatibility val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion" val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" - val composeCoil = "io.coil-kt:coil-compose:2.6.0" + val composeCoil = "io.coil-kt:coil-compose:2.0.0" val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.8.2" + val sentryNativeNdk = "io.sentry:sentry-native-ndk:0.7.14" + object OpenTelemetry { - val otelVersion = "1.33.0" + val otelVersion = "1.44.1" val otelAlphaVersion = "$otelVersion-alpha" - val otelJavaagentVersion = "1.32.0" - val otelJavaagentAlphaVersion = "$otelJavaagentVersion-alpha" - val otelSemanticConvetionsVersion = "1.23.1-alpha" + val otelInstrumentationVersion = "2.10.0" + val otelInstrumentationAlphaVersion = "$otelInstrumentationVersion-alpha" + val otelSemanticConvetionsVersion = "1.28.0-alpha" // check https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/dependencyManagement/build.gradle.kts#L49 for release version above to find a compatible version val otelSdk = "io.opentelemetry:opentelemetry-sdk:$otelVersion" val otelSemconv = "io.opentelemetry.semconv:opentelemetry-semconv:$otelSemanticConvetionsVersion" - val otelJavaAgent = "io.opentelemetry.javaagent:opentelemetry-javaagent:$otelJavaagentVersion" - val otelJavaAgentExtensionApi = "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:$otelJavaagentAlphaVersion" - val otelJavaAgentTooling = "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:$otelJavaagentAlphaVersion" + val otelSemconvIncubating = "io.opentelemetry.semconv:opentelemetry-semconv-incubating:$otelSemanticConvetionsVersion" + val otelJavaAgent = "io.opentelemetry.javaagent:opentelemetry-javaagent:$otelInstrumentationVersion" + val otelJavaAgentExtensionApi = "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:$otelInstrumentationAlphaVersion" + val otelJavaAgentTooling = "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:$otelInstrumentationAlphaVersion" val otelExtensionAutoconfigureSpi = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:$otelVersion" + val otelExtensionAutoconfigure = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:$otelVersion" + val otelInstrumentationBom = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:$otelInstrumentationVersion" } } @@ -206,7 +209,9 @@ object Config { object QualityPlugins { object Jacoco { val version = "0.8.7" - val minimumCoverage = BigDecimal.valueOf(0.6) + + // TODO [POTEL] add tests and restore + val minimumCoverage = BigDecimal.valueOf(0.1) } val spotless = "com.diffplug.spotless" val spotlessVersion = "6.11.0" @@ -241,6 +246,7 @@ object Config { val SENTRY_APOLLO3_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo3" val SENTRY_APOLLO_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo" val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql" + val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22" val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" @@ -261,9 +267,4 @@ object Config { val errorprone = "com.google.errorprone:error_prone_core:2.11.0" val errorProneNullAway = "com.uber.nullaway:nullaway:0.9.5" } - - object NativePlugins { - val nativeBundlePlugin = "io.github.howardpang:androidNativeBundle:1.1.1" - val nativeBundleExport = "com.ydq.android.gradle.native-aar.export" - } } diff --git a/gradle.properties b/gradle.properties index 65fe48ea942..8764c2cdf94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.20.0 +versionName=8.0.0-rc.3 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android diff --git a/scripts/update-sentry-native-ndk.sh b/scripts/update-sentry-native-ndk.sh new file mode 100755 index 00000000000..347e5df921b --- /dev/null +++ b/scripts/update-sentry-native-ndk.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd $(dirname "$0")/../ +GRADLE_NDK_FILEPATH=buildSrc/src/main/java/Config.kt + +case $1 in +get-version) + version=$(perl -ne 'print "$1\n" if ( m/io\.sentry:sentry-native-ndk:([0-9.]+)+/ )' $GRADLE_NDK_FILEPATH) + + echo "v$version" + ;; +get-repo) + echo "https://github.com/getsentry/sentry-native.git" + ;; +set-version) + version=$2 + + # Remove leading "v" + if [[ "$version" == v* ]]; then + version="${version:1}" + fi + + echo "Setting sentry-native-ndk version to '$version'" + + PATTERN="io\.sentry:sentry-native-ndk:([0-9.]+)+" + perl -pi -e "s/$PATTERN/io.sentry:sentry-native-ndk:$version/g" $GRADLE_NDK_FILEPATH + ;; +*) + echo "Unknown argument $1" + exit 1 + ;; +esac diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index f6760e19419..3f34efa9d55 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -8,12 +8,12 @@ public final class io/sentry/android/core/ActivityBreadcrumbsIntegration : andro 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 register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/ActivityFramesTracker { - public fun (Lio/sentry/android/core/LoadClass;Lio/sentry/android/core/SentryAndroidOptions;)V - public fun (Lio/sentry/android/core/LoadClass;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/MainLooperHandler;)V + public fun (Lio/sentry/util/LoadClass;Lio/sentry/android/core/SentryAndroidOptions;)V + public fun (Lio/sentry/util/LoadClass;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/MainLooperHandler;)V public fun addActivity (Landroid/app/Activity;)V public fun isFrameMetricsAggregatorAvailable ()Z public fun setMetrics (Landroid/app/Activity;Lio/sentry/protocol/SentryId;)V @@ -37,11 +37,11 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android 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 register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector { - public fun (Lio/sentry/ILogger;Lio/sentry/android/core/BuildInfoProvider;)V + public fun (Lio/sentry/ILogger;)V public fun collect (Lio/sentry/PerformanceCollectionData;)V public fun setup ()V } @@ -67,7 +67,8 @@ public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerforma } public class io/sentry/android/core/AndroidProfiler { - public fun (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;Lio/sentry/android/core/BuildInfoProvider;)V + protected final field lock Lio/sentry/util/AutoClosableReentrantLock; + public fun (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;)V public fun close ()V public fun endAndCollect (ZLjava/util/List;)Lio/sentry/android/core/AndroidProfiler$ProfileEndData; public fun start ()Lio/sentry/android/core/AndroidProfiler$ProfileStartData; @@ -92,7 +93,7 @@ public class io/sentry/android/core/AndroidProfiler$ProfileStartData { public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;)V public fun close ()V - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public final fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/AnrIntegrationFactory { @@ -102,6 +103,7 @@ public final class io/sentry/android/core/AnrIntegrationFactory { public final class io/sentry/android/core/AnrV2EventProcessor : io/sentry/BackfillingEventProcessor { public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V + public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } @@ -109,7 +111,7 @@ public final class io/sentry/android/core/AnrV2EventProcessor : io/sentry/Backfi public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;)V public fun close ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/AbnormalExit, io/sentry/hints/Backfillable { @@ -128,13 +130,13 @@ public final class io/sentry/android/core/AppComponentsBreadcrumbsIntegration : public fun onConfigurationChanged (Landroid/content/res/Configuration;)V public fun onLowMemory ()V public fun onTrimMemory (I)V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/Integration, java/io/Closeable { public fun ()V public fun close ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/AppState { @@ -183,7 +185,7 @@ public final class io/sentry/android/core/CurrentActivityIntegration : android/a 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 register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/DeviceInfoUtil { @@ -198,10 +200,11 @@ public final class io/sentry/android/core/DeviceInfoUtil { } public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable { + protected final field startLock Lio/sentry/util/AutoClosableReentrantLock; public fun ()V public fun close ()V public static fun getOutboxFileObserver ()Lio/sentry/android/core/EnvelopeFileObserverIntegration; - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public final fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public abstract interface class io/sentry/android/core/IDebugImagesLoader { @@ -217,7 +220,7 @@ public final class io/sentry/android/core/InternalSentrySdk { public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map; } -public final class io/sentry/android/core/LoadClass { +public final class io/sentry/android/core/LoadClass : io/sentry/util/LoadClass { public fun ()V public fun isClassAvailable (Ljava/lang/String;Lio/sentry/ILogger;)Z public fun isClassAvailable (Ljava/lang/String;Lio/sentry/SentryOptions;)Z @@ -236,23 +239,24 @@ public final class io/sentry/android/core/NdkIntegration : io/sentry/Integration public static final field SENTRY_NDK_CLASS_NAME Ljava/lang/String; public fun (Ljava/lang/Class;)V public fun close ()V - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public final fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/NetworkBreadcrumbsIntegration : io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/ILogger;)V public fun close ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;)V public fun close ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V + public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } @@ -279,7 +283,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector; public fun getNativeSdkName ()Ljava/lang/String; public fun getNdkHandlerStrategy ()I - public fun getProfilingTracesIntervalMillis ()I public fun getStartupCrashDurationThresholdMillis ()J public fun isAnrEnabled ()Z public fun isAnrReportInDebug ()Z @@ -325,7 +328,6 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V public fun setNativeSdkName (Ljava/lang/String;)V - public fun setProfilingTracesIntervalMillis (I)V public fun setReportHistoricalAnrs (Z)V } @@ -368,6 +370,7 @@ public final class io/sentry/android/core/SentryPerformanceProvider { } public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener { + protected final field lock Lio/sentry/util/AutoClosableReentrantLock; public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V public fun clear ()V public fun onFrameMetricCollected (JJJJZZF)V @@ -379,7 +382,7 @@ public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : i public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Ljava/util/List;)V public fun close ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/TempSensorBreadcrumbsIntegration : android/hardware/SensorEventListener, io/sentry/Integration, java/io/Closeable { @@ -387,11 +390,11 @@ public final class io/sentry/android/core/TempSensorBreadcrumbsIntegration : and public fun close ()V public fun onAccuracyChanged (Landroid/hardware/Sensor;I)V public fun onSensorChanged (Landroid/hardware/SensorEvent;)V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { - public fun (Landroid/app/Application;Lio/sentry/android/core/LoadClass;)V + public fun (Landroid/app/Application;Lio/sentry/util/LoadClass;)V public fun close ()V public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V @@ -400,18 +403,19 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a 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 register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;)V + public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; - public static fun snapshotViewHierarchy (Landroid/app/Activity;Ljava/util/List;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; + public static fun snapshotViewHierarchy (Landroid/app/Activity;Ljava/util/List;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/view/View;Ljava/util/List;)Lio/sentry/protocol/ViewHierarchy; - public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B + public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B } public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache { @@ -457,6 +461,7 @@ public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java } public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter { + public static final field staticLock Lio/sentry/util/AutoClosableReentrantLock; public fun ()V public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V public fun clear ()V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java index d9bfb5cea36..3cd7709c4be 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java @@ -8,10 +8,12 @@ import android.os.Bundle; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -23,21 +25,23 @@ public final class ActivityBreadcrumbsIntegration implements Integration, Closeable, Application.ActivityLifecycleCallbacks { private final @NotNull Application application; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private boolean enabled; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + // TODO check if locking is even required at all for lifecycle methods public ActivityBreadcrumbsIntegration(final @NotNull Application application) { this.application = Objects.requireNonNull(application, "Application is required"); } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { final SentryAndroidOptions androidOptions = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, "SentryAndroidOptions is required"); - this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.enabled = androidOptions.isEnableActivityLifecycleBreadcrumbs(); options .getLogger() @@ -54,8 +58,9 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio public void close() throws IOException { if (enabled) { application.unregisterActivityLifecycleCallbacks(this); - if (hub != null) { - hub.getOptions() + if (scopes != null) { + scopes + .getOptions() .getLogger() .log(SentryLevel.DEBUG, "ActivityBreadcrumbsIntegration removed."); } @@ -63,44 +68,58 @@ public void close() throws IOException { } @Override - public synchronized void onActivityCreated( + public void onActivityCreated( final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) { - addBreadcrumb(activity, "created"); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "created"); + } } @Override - public synchronized void onActivityStarted(final @NotNull Activity activity) { - addBreadcrumb(activity, "started"); + public void onActivityStarted(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "started"); + } } @Override - public synchronized void onActivityResumed(final @NotNull Activity activity) { - addBreadcrumb(activity, "resumed"); + public void onActivityResumed(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "resumed"); + } } @Override - public synchronized void onActivityPaused(final @NotNull Activity activity) { - addBreadcrumb(activity, "paused"); + public void onActivityPaused(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "paused"); + } } @Override - public synchronized void onActivityStopped(final @NotNull Activity activity) { - addBreadcrumb(activity, "stopped"); + public void onActivityStopped(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "stopped"); + } } @Override - public synchronized void onActivitySaveInstanceState( + public void onActivitySaveInstanceState( final @NotNull Activity activity, final @NotNull Bundle outState) { - addBreadcrumb(activity, "saveInstanceState"); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "saveInstanceState"); + } } @Override - public synchronized void onActivityDestroyed(final @NotNull Activity activity) { - addBreadcrumb(activity, "destroyed"); + public void onActivityDestroyed(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + addBreadcrumb(activity, "destroyed"); + } } private void addBreadcrumb(final @NotNull Activity activity, final @NotNull String state) { - if (hub == null) { + if (scopes == null) { return; } @@ -114,7 +133,7 @@ private void addBreadcrumb(final @NotNull Activity activity, final @NotNull Stri final Hint hint = new Hint(); hint.set(ANDROID_ACTIVITY, activity); - hub.addBreadcrumb(breadcrumb, hint); + scopes.addBreadcrumb(breadcrumb, hint); } private @NotNull String getActivityName(final @NotNull Activity activity) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java index 93f50b3ec14..ade8fdd37c7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java @@ -3,11 +3,13 @@ import android.app.Activity; import android.util.SparseIntArray; import androidx.core.app.FrameMetricsAggregator; +import io.sentry.ISentryLifecycleToken; import io.sentry.MeasurementUnit; import io.sentry.SentryLevel; -import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; +import io.sentry.util.AutoClosableReentrantLock; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; @@ -37,9 +39,10 @@ public final class ActivityFramesTracker { new WeakHashMap<>(); private final @NotNull MainLooperHandler handler; + protected @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public ActivityFramesTracker( - final @NotNull LoadClass loadClass, + final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull SentryAndroidOptions options, final @NotNull MainLooperHandler handler) { @@ -54,13 +57,14 @@ public ActivityFramesTracker( } public ActivityFramesTracker( - final @NotNull LoadClass loadClass, final @NotNull SentryAndroidOptions options) { + final @NotNull io.sentry.util.LoadClass loadClass, + final @NotNull SentryAndroidOptions options) { this(loadClass, options, new MainLooperHandler()); } @TestOnly ActivityFramesTracker( - final @NotNull LoadClass loadClass, + final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull SentryAndroidOptions options, final @NotNull MainLooperHandler handler, final @Nullable FrameMetricsAggregator frameMetricsAggregator) { @@ -77,13 +81,15 @@ public boolean isFrameMetricsAggregatorAvailable() { } @SuppressWarnings("NullAway") - public synchronized void addActivity(final @NotNull Activity activity) { - if (!isFrameMetricsAggregatorAvailable()) { - return; - } + public void addActivity(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isFrameMetricsAggregatorAvailable()) { + return; + } - runSafelyOnUiThread(() -> frameMetricsAggregator.add(activity), "FrameMetricsAggregator.add"); - snapshotFrameCountsAtStart(activity); + runSafelyOnUiThread(() -> frameMetricsAggregator.add(activity), "FrameMetricsAggregator.add"); + snapshotFrameCountsAtStart(activity); + } } private void snapshotFrameCountsAtStart(final @NotNull Activity activity) { @@ -131,45 +137,46 @@ private void snapshotFrameCountsAtStart(final @NotNull Activity activity) { } @SuppressWarnings("NullAway") - public synchronized void setMetrics( - final @NotNull Activity activity, final @NotNull SentryId transactionId) { - if (!isFrameMetricsAggregatorAvailable()) { - return; - } + public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId transactionId) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isFrameMetricsAggregatorAvailable()) { + return; + } - // NOTE: removing an activity does not reset the frame counts, only reset() does - // throws IllegalArgumentException when attempting to remove - // OnFrameMetricsAvailableListener - // that was never added. - // there's no contains method. - // throws NullPointerException when attempting to remove - // OnFrameMetricsAvailableListener and - // there was no - // Observers, See - // https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b - runSafelyOnUiThread(() -> frameMetricsAggregator.remove(activity), null); - - final @Nullable FrameCounts frameCounts = diffFrameCountsAtEnd(activity); - - if (frameCounts == null - || (frameCounts.totalFrames == 0 - && frameCounts.slowFrames == 0 - && frameCounts.frozenFrames == 0)) { - return; - } + // NOTE: removing an activity does not reset the frame counts, only reset() does + // throws IllegalArgumentException when attempting to remove + // OnFrameMetricsAvailableListener + // that was never added. + // there's no contains method. + // throws NullPointerException when attempting to remove + // OnFrameMetricsAvailableListener and + // there was no + // Observers, See + // https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b + runSafelyOnUiThread(() -> frameMetricsAggregator.remove(activity), null); + + final @Nullable FrameCounts frameCounts = diffFrameCountsAtEnd(activity); + + if (frameCounts == null + || (frameCounts.totalFrames == 0 + && frameCounts.slowFrames == 0 + && frameCounts.frozenFrames == 0)) { + return; + } - final MeasurementValue tfValues = - new MeasurementValue(frameCounts.totalFrames, MeasurementUnit.NONE); - final MeasurementValue sfValues = - new MeasurementValue(frameCounts.slowFrames, MeasurementUnit.NONE); - final MeasurementValue ffValues = - new MeasurementValue(frameCounts.frozenFrames, MeasurementUnit.NONE); - final Map measurements = new HashMap<>(); - measurements.put(MeasurementValue.KEY_FRAMES_TOTAL, tfValues); - measurements.put(MeasurementValue.KEY_FRAMES_SLOW, sfValues); - measurements.put(MeasurementValue.KEY_FRAMES_FROZEN, ffValues); - - activityMeasurements.put(transactionId, measurements); + final MeasurementValue tfValues = + new MeasurementValue(frameCounts.totalFrames, MeasurementUnit.NONE); + final MeasurementValue sfValues = + new MeasurementValue(frameCounts.slowFrames, MeasurementUnit.NONE); + final MeasurementValue ffValues = + new MeasurementValue(frameCounts.frozenFrames, MeasurementUnit.NONE); + final Map measurements = new HashMap<>(); + measurements.put(MeasurementValue.KEY_FRAMES_TOTAL, tfValues); + measurements.put(MeasurementValue.KEY_FRAMES_SLOW, sfValues); + measurements.put(MeasurementValue.KEY_FRAMES_FROZEN, ffValues); + + activityMeasurements.put(transactionId, measurements); + } } private @Nullable FrameCounts diffFrameCountsAtEnd(final @NotNull Activity activity) { @@ -191,30 +198,33 @@ public synchronized void setMetrics( } @Nullable - public synchronized Map takeMetrics( - final @NotNull SentryId transactionId) { - if (!isFrameMetricsAggregatorAvailable()) { - return null; - } + public Map takeMetrics(final @NotNull SentryId transactionId) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isFrameMetricsAggregatorAvailable()) { + return null; + } - final Map stringMeasurementValueMap = - activityMeasurements.get(transactionId); - activityMeasurements.remove(transactionId); - return stringMeasurementValueMap; + final Map stringMeasurementValueMap = + activityMeasurements.get(transactionId); + activityMeasurements.remove(transactionId); + return stringMeasurementValueMap; + } } @SuppressWarnings("NullAway") - public synchronized void stop() { - if (isFrameMetricsAggregatorAvailable()) { - runSafelyOnUiThread(() -> frameMetricsAggregator.stop(), "FrameMetricsAggregator.stop"); - frameMetricsAggregator.reset(); + public void stop() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (isFrameMetricsAggregatorAvailable()) { + runSafelyOnUiThread(() -> frameMetricsAggregator.stop(), "FrameMetricsAggregator.stop"); + frameMetricsAggregator.reset(); + } + activityMeasurements.clear(); } - activityMeasurements.clear(); } private void runSafelyOnUiThread(final Runnable runnable, final String tag) { try { - if (AndroidMainThreadChecker.getInstance().isMainThread()) { + if (AndroidThreadChecker.getInstance().isMainThread()) { runnable.run(); } else { handler.post( 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 14141bf4bba..eaf05e619dc 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 @@ -11,8 +11,9 @@ import android.os.Looper; import android.os.SystemClock; import io.sentry.FullyDisplayedReporter; -import io.sentry.IHub; import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.ISpan; import io.sentry.ITransaction; import io.sentry.Instrumenter; @@ -22,6 +23,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryNanotimeDate; import io.sentry.SentryOptions; +import io.sentry.SpanOptions; import io.sentry.SpanStatus; import io.sentry.TracesSamplingDecision; import io.sentry.TransactionContext; @@ -33,6 +35,7 @@ import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.TransactionNameSource; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import java.io.Closeable; @@ -62,7 +65,7 @@ public final class ActivityLifecycleIntegration private final @NotNull Application application; private final @NotNull BuildInfoProvider buildInfoProvider; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryAndroidOptions options; private boolean performanceEnabled = false; @@ -89,6 +92,7 @@ public final class ActivityLifecycleIntegration new WeakHashMap<>(); private final @NotNull ActivityFramesTracker activityFramesTracker; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public ActivityLifecycleIntegration( final @NotNull Application application, @@ -106,13 +110,13 @@ public ActivityLifecycleIntegration( } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, "SentryAndroidOptions is required"); - this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); performanceEnabled = isPerformanceEnabled(this.options); fullyDisplayedReporter = this.options.getFullyDisplayedReporter(); @@ -154,10 +158,10 @@ private void stopPreviousTransactions() { private void startTracing(final @NotNull Activity activity) { WeakReference weakActivity = new WeakReference<>(activity); - if (hub != null && !isRunningTransactionOrTrace(activity)) { + if (scopes != null && !isRunningTransactionOrTrace(activity)) { if (!performanceEnabled) { activitiesWithOngoingTransactions.put(activity, NoOpTransaction.getInstance()); - TracingUtils.startNewTrace(hub); + TracingUtils.startNewTrace(scopes); } else { // as we allow a single transaction running on the bound Scope, we finish the previous ones stopPreviousTransactions(); @@ -226,17 +230,20 @@ private void startTracing(final @NotNull Activity activity) { } transactionOptions.setStartTimestamp(ttidStartTime); transactionOptions.setAppStartTransaction(appStartSamplingDecision != null); + setSpanOrigin(transactionOptions); // we can only bind to the scope if there's no running transaction ITransaction transaction = - hub.startTransaction( + scopes.startTransaction( new TransactionContext( activityName, TransactionNameSource.COMPONENT, UI_LOAD_OP, appStartSamplingDecision), transactionOptions); - setSpanOrigin(transaction); + + final SpanOptions spanOptions = new SpanOptions(); + setSpanOrigin(spanOptions); // in case appStartTime isn't available, we don't create a span for it. if (!(firstActivityCreated || appStartTime == null || coldStart == null)) { @@ -246,8 +253,8 @@ private void startTracing(final @NotNull Activity activity) { getAppStartOp(coldStart), getAppStartDesc(coldStart), appStartTime, - Instrumenter.SENTRY); - setSpanOrigin(appStartSpan); + Instrumenter.SENTRY, + spanOptions); // in case there's already an end time (e.g. due to deferred SDK init) // we can finish the app-start span @@ -255,15 +262,21 @@ private void startTracing(final @NotNull Activity activity) { } final @NotNull ISpan ttidSpan = transaction.startChild( - TTID_OP, getTtidDesc(activityName), ttidStartTime, Instrumenter.SENTRY); + TTID_OP, + getTtidDesc(activityName), + ttidStartTime, + Instrumenter.SENTRY, + spanOptions); ttidSpanMap.put(activity, ttidSpan); - setSpanOrigin(ttidSpan); if (timeToFullDisplaySpanEnabled && fullyDisplayedReporter != null && options != null) { final @NotNull ISpan ttfdSpan = transaction.startChild( - TTFD_OP, getTtfdDesc(activityName), ttidStartTime, Instrumenter.SENTRY); - setSpanOrigin(ttfdSpan); + TTFD_OP, + getTtfdDesc(activityName), + ttidStartTime, + Instrumenter.SENTRY, + spanOptions); try { ttfdSpanMap.put(activity, ttfdSpan); ttfdAutoCloseFuture = @@ -282,7 +295,7 @@ private void startTracing(final @NotNull Activity activity) { } // lets bind to the scope so other integrations can pick it up - hub.configureScope( + scopes.configureScope( scope -> { applyScope(scope, transaction); }); @@ -292,10 +305,8 @@ private void startTracing(final @NotNull Activity activity) { } } - private void setSpanOrigin(ISpan span) { - if (span != null) { - span.getSpanContext().setOrigin(TRACE_ORIGIN); - } + private void setSpanOrigin(final @NotNull SpanOptions spanOptions) { + spanOptions.setOrigin(TRACE_ORIGIN); } @VisibleForTesting @@ -360,10 +371,10 @@ private void finishTransaction( status = SpanStatus.OK; } transaction.finish(status); - if (hub != null) { + if (scopes != 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( + scopes.configureScope( scope -> { clearScope(scope, transaction); }); @@ -384,31 +395,33 @@ public void onActivityPreCreated( return; } lastPausedTime = - hub != null - ? hub.getOptions().getDateProvider().now() + scopes != null + ? scopes.getOptions().getDateProvider().now() : AndroidDateUtils.getCurrentSentryDateTime(); lastPausedUptimeMillis = SystemClock.uptimeMillis(); helper.setOnCreateStartTimestamp(lastPausedTime); } @Override - public synchronized void onActivityCreated( + public void onActivityCreated( final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) { if (!isAllActivityCallbacksAvailable) { onActivityPreCreated(activity, savedInstanceState); } - setColdStart(savedInstanceState); - if (hub != null && options != null && options.isEnableScreenTracking()) { - final @Nullable String activityClassName = ClassUtil.getClassName(activity); - hub.configureScope(scope -> scope.setScreen(activityClassName)); - } - startTracing(activity); - final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + setColdStart(savedInstanceState); + if (scopes != null && options != null && options.isEnableScreenTracking()) { + final @Nullable String activityClassName = ClassUtil.getClassName(activity); + scopes.configureScope(scope -> scope.setScreen(activityClassName)); + } + startTracing(activity); + final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); - firstActivityCreated = true; + firstActivityCreated = true; - if (performanceEnabled && ttfdSpan != null && fullyDisplayedReporter != null) { - fullyDisplayedReporter.registerFullyDrawnListener(() -> onFullFrameDrawn(ttfdSpan)); + if (performanceEnabled && ttfdSpan != null && fullyDisplayedReporter != null) { + fullyDisplayedReporter.registerFullyDrawnListener(() -> onFullFrameDrawn(ttfdSpan)); + } } } @@ -433,19 +446,21 @@ public void onActivityPreStarted(final @NotNull Activity activity) { } @Override - public synchronized void onActivityStarted(final @NotNull Activity activity) { - if (!isAllActivityCallbacksAvailable) { - onActivityPostCreated(activity, null); - onActivityPreStarted(activity); - } - if (performanceEnabled) { - // The docs on the screen rendering performance tracing - // (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition), - // state that the tracing starts for every Activity class when the app calls - // .onActivityStarted. - // Adding an Activity in onActivityCreated leads to Window.FEATURE_NO_TITLE not - // working. Moving this to onActivityStarted fixes the problem. - activityFramesTracker.addActivity(activity); + public void onActivityStarted(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isAllActivityCallbacksAvailable) { + onActivityPostCreated(activity, null); + onActivityPreStarted(activity); + } + if (performanceEnabled) { + // The docs on the screen rendering performance tracing + // (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition), + // state that the tracing starts for every Activity class when the app calls + // .onActivityStarted. + // Adding an Activity in onActivityCreated leads to Window.FEATURE_NO_TITLE not + // working. Moving this to onActivityStarted fixes the problem. + activityFramesTracker.addActivity(activity); + } } } @@ -460,20 +475,23 @@ public void onActivityPostStarted(final @NotNull Activity activity) { } @Override - public synchronized void onActivityResumed(final @NotNull Activity activity) { - if (!isAllActivityCallbacksAvailable) { - onActivityPostStarted(activity); - } - if (performanceEnabled) { - final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); - final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); - if (activity.getWindow() != null) { - FirstDrawDoneListener.registerForNextDraw( - activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider); - } else { - // Posting a task to the main thread's handler will make it executed after it finished - // its current job. That is, right after the activity draws the layout. - new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan)); + public void onActivityResumed(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isAllActivityCallbacksAvailable) { + onActivityPostStarted(activity); + } + if (performanceEnabled) { + + final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); + final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); + if (activity.getWindow() != null) { + FirstDrawDoneListener.registerForNextDraw( + activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider); + } else { + // Posting a task to the main thread's handler will make it executed after it finished + // its current job. That is, right after the activity draws the layout. + new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan)); + } } } } @@ -491,65 +509,73 @@ public void onActivityPrePaused(@NotNull Activity activity) { // this ensures any newly launched activity will not use the app start timestamp as txn start firstActivityCreated = true; lastPausedTime = - hub != null - ? hub.getOptions().getDateProvider().now() + scopes != null + ? scopes.getOptions().getDateProvider().now() : AndroidDateUtils.getCurrentSentryDateTime(); lastPausedUptimeMillis = SystemClock.uptimeMillis(); } @Override - public synchronized void onActivityPaused(final @NotNull Activity activity) { - // only executed if API < 29 otherwise it happens on onActivityPrePaused - if (!isAllActivityCallbacksAvailable) { - onActivityPrePaused(activity); + public void onActivityPaused(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // only executed if API < 29 otherwise it happens on onActivityPrePaused + if (!isAllActivityCallbacksAvailable) { + onActivityPrePaused(activity); + } } } @Override - public void onActivityStopped(final @NotNull Activity activity) {} + public void onActivityStopped(final @NotNull Activity activity) { + // no-op (acquire lock if this no longer is no-op) + } @Override public void onActivitySaveInstanceState( - final @NotNull Activity activity, final @NotNull Bundle outState) {} + final @NotNull Activity activity, final @NotNull Bundle outState) { + // no-op (acquire lock if this no longer is no-op) + } @Override - public synchronized void onActivityDestroyed(final @NotNull Activity activity) { - final ActivityLifecycleSpanHelper helper = activitySpanHelpers.remove(activity); - if (helper != null) { - helper.clear(); - } - if (performanceEnabled) { + public void onActivityDestroyed(final @NotNull Activity activity) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.remove(activity); + if (helper != null) { + helper.clear(); + } + if (performanceEnabled) { - // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid - // memory leak - finishSpan(appStartSpan, SpanStatus.CANCELLED); + // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid + // memory leak + finishSpan(appStartSpan, SpanStatus.CANCELLED); - // we finish the ttidSpan as cancelled in case it isn't completed yet - final ISpan ttidSpan = ttidSpanMap.get(activity); - final ISpan ttfdSpan = ttfdSpanMap.get(activity); - finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED); + // we finish the ttidSpan as cancelled in case it isn't completed yet + final ISpan ttidSpan = ttidSpanMap.get(activity); + final ISpan ttfdSpan = ttfdSpanMap.get(activity); + finishSpan(ttidSpan, SpanStatus.DEADLINE_EXCEEDED); - // we finish the ttfdSpan as deadline_exceeded in case it isn't completed yet - finishExceededTtfdSpan(ttfdSpan, ttidSpan); - cancelTtfdAutoClose(); + // we finish the ttfdSpan as deadline_exceeded in case it isn't completed yet + finishExceededTtfdSpan(ttfdSpan, ttidSpan); + cancelTtfdAutoClose(); - // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it, - // we make sure to finish it when the activity gets destroyed. - stopTracing(activity, true); + // in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it, + // we make sure to finish it when the activity gets destroyed. + stopTracing(activity, true); - // set it to null in case its been just finished as cancelled - appStartSpan = null; - ttidSpanMap.remove(activity); - ttfdSpanMap.remove(activity); - } + // set it to null in case its been just finished as cancelled + appStartSpan = null; + ttidSpanMap.remove(activity); + ttfdSpanMap.remove(activity); + } - // clear it up, so we don't start again for the same activity if the activity is in the - // activity stack still. - // if the activity is opened again and not in memory, transactions will be created normally. - activitiesWithOngoingTransactions.remove(activity); + // clear it up, so we don't start again for the same activity if the activity is in the + // activity stack still. + // if the activity is opened again and not in memory, transactions will be created normally. + activitiesWithOngoingTransactions.remove(activity); - if (activitiesWithOngoingTransactions.isEmpty()) { - clear(); + if (activitiesWithOngoingTransactions.isEmpty()) { + clear(); + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java index 8f54305e6fe..ccd878761db 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java @@ -1,7 +1,5 @@ package io.sentry.android.core; -import android.annotation.SuppressLint; -import android.os.Build; import android.os.SystemClock; import android.system.Os; import android.system.OsConstants; @@ -41,24 +39,15 @@ public final class AndroidCpuCollector implements IPerformanceSnapshotCollector private final @NotNull File selfStat = new File("/proc/self/stat"); private final @NotNull ILogger logger; - private final @NotNull BuildInfoProvider buildInfoProvider; private boolean isEnabled = false; private final @NotNull Pattern newLinePattern = Pattern.compile("[\n\t\r ]"); - public AndroidCpuCollector( - final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { + public AndroidCpuCollector(final @NotNull ILogger logger) { this.logger = Objects.requireNonNull(logger, "Logger is required."); - this.buildInfoProvider = - Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required."); } - @SuppressLint("NewApi") @Override public void setup() { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - isEnabled = false; - return; - } isEnabled = true; clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK); numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF); @@ -66,10 +55,9 @@ public void setup() { lastCpuNanos = readTotalCpuNanos(); } - @SuppressLint("NewApi") @Override public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP || !isEnabled) { + if (!isEnabled) { return; } final long nowNanos = SystemClock.elapsedRealtimeNanos(); 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 d5dfce77b28..5d4658bcf9b 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 @@ -8,17 +8,20 @@ import io.sentry.DeduplicateMultithreadedEventProcessor; import io.sentry.DefaultTransactionPerformanceCollector; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.NoOpConnectionStatusProvider; +import io.sentry.ScopeType; import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; +import io.sentry.SentryOpenTelemetryMode; import io.sentry.android.core.cache.AndroidEnvelopeCache; import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; -import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.fragment.FragmentLifecycleIntegration; @@ -98,6 +101,8 @@ static void loadDefaultAndMetadataOptions( // Firstly set the logger, if `debug=true` configured, logging can start asap. options.setLogger(logger); + options.setDefaultScopeType(ScopeType.CURRENT); + options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF); options.setDateProvider(new SentryAndroidDateProvider()); // set a lower flush timeout on Android to avoid ANRs @@ -116,7 +121,7 @@ static void loadDefaultAndMetadataOptions( static void initializeIntegrationsAndProcessors( final @NotNull SentryAndroidOptions options, final @NotNull Context context, - final @NotNull LoadClass loadClass, + final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker) { initializeIntegrationsAndProcessors( options, @@ -130,7 +135,7 @@ static void initializeIntegrationsAndProcessors( final @NotNull SentryAndroidOptions options, final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider, - final @NotNull LoadClass loadClass, + final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker) { if (options.getCacheDirPath() != null @@ -155,7 +160,7 @@ static void initializeIntegrationsAndProcessors( // Check if the profiler was already instantiated in the app start. // We use the Android profiler, that uses a global start/stop api, so we need to preserve the // state of the profiler, and it's only possible retaining the instance. - synchronized (AppStartMetrics.getInstance()) { + try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) { final @Nullable ITransactionProfiler appStartProfiler = AppStartMetrics.getInstance().getAppStartProfiler(); if (appStartProfiler != null) { @@ -205,11 +210,10 @@ static void initializeIntegrationsAndProcessors( options.setViewHierarchyExporters(viewHierarchyExporters); } - options.setMainThreadChecker(AndroidMainThreadChecker.getInstance()); + options.setThreadChecker(AndroidThreadChecker.getInstance()); if (options.getPerformanceCollectors().isEmpty()) { options.addPerformanceCollector(new AndroidMemoryCollector()); - options.addPerformanceCollector( - new AndroidCpuCollector(options.getLogger(), buildInfoProvider)); + options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger())); if (options.isEnablePerformanceV2()) { options.addPerformanceCollector( @@ -234,7 +238,7 @@ static void installDefaultIntegrations( final @NotNull Context context, final @NotNull SentryAndroidOptions options, final @NotNull BuildInfoProvider buildInfoProvider, - final @NotNull LoadClass loadClass, + final @NotNull io.sentry.util.LoadClass loadClass, final @NotNull ActivityFramesTracker activityFramesTracker, final boolean isFragmentAvailable, final boolean isTimberAvailable, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index d24025c5516..c1ae1237e23 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -1,7 +1,6 @@ package io.sentry.android.core; import android.annotation.SuppressLint; -import android.os.Build; import android.os.Debug; import android.os.Process; import android.os.SystemClock; @@ -9,12 +8,15 @@ import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; import io.sentry.SentryLevel; +import io.sentry.SentryUUID; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; import io.sentry.profilemeasurements.ProfileMeasurementValue; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.File; import java.util.ArrayDeque; @@ -22,7 +24,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; @@ -92,18 +93,17 @@ public ProfileEndData( private final @NotNull ArrayDeque frozenFrameRenderMeasurements = new ArrayDeque<>(); private final @NotNull Map measurementsMap = new HashMap<>(); - private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull ISentryExecutorService executorService; private final @NotNull ILogger logger; private boolean isRunning = false; + protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public AndroidProfiler( final @NotNull String tracesFilesDirPath, final int intervalUs, final @NotNull SentryFrameMetricsCollector frameMetricsCollector, final @NotNull ISentryExecutorService executorService, - final @NotNull ILogger logger, - final @NotNull BuildInfoProvider buildInfoProvider) { + final @NotNull ILogger logger) { this.traceFilesDir = new File(Objects.requireNonNull(tracesFilesDirPath, "TracesFilesDirPath is required")); this.intervalUs = intervalUs; @@ -111,181 +111,181 @@ public AndroidProfiler( this.executorService = Objects.requireNonNull(executorService, "ExecutorService is required."); this.frameMetricsCollector = Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required"); - this.buildInfoProvider = - Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required."); } @SuppressLint("NewApi") - public synchronized @Nullable ProfileStartData start() { - // intervalUs is 0 only if there was a problem in the init - if (intervalUs == 0) { - logger.log( - SentryLevel.WARNING, "Disabling profiling because intervaUs is set to %d", intervalUs); - return null; - } - - if (isRunning) { - logger.log(SentryLevel.WARNING, "Profiling has already started..."); - return null; - } - - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return null; - - // We create a file with a uuid name, so no need to check if it already exists - traceFile = new File(traceFilesDir, UUID.randomUUID() + ".trace"); - - measurementsMap.clear(); - screenFrameRateMeasurements.clear(); - slowFrameRenderMeasurements.clear(); - frozenFrameRenderMeasurements.clear(); - - frameMetricsCollectorId = - frameMetricsCollector.startCollection( - new SentryFrameMetricsCollector.FrameMetricsCollectorListener() { - float lastRefreshRate = 0; + public @Nullable ProfileStartData start() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // intervalUs is 0 only if there was a problem in the init + if (intervalUs == 0) { + logger.log( + SentryLevel.WARNING, "Disabling profiling because intervaUs is set to %d", intervalUs); + return null; + } - @Override - public void onFrameMetricCollected( - final long frameStartNanos, - final long frameEndNanos, - final long durationNanos, - final long delayNanos, - final boolean isSlow, - final boolean isFrozen, - final float refreshRate) { - // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), - // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp - // relative to profileStartNanos - final long frameTimestampRelativeNanos = - frameEndNanos - - System.nanoTime() - + SystemClock.elapsedRealtimeNanos() - - profileStartNanos; + if (isRunning) { + logger.log(SentryLevel.WARNING, "Profiling has already started..."); + return null; + } - // We don't allow negative relative timestamps. - // So we add a check, even if this should never happen. - if (frameTimestampRelativeNanos < 0) { - return; - } - if (isFrozen) { - frozenFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); - } else if (isSlow) { - slowFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + // We create a file with a uuid name, so no need to check if it already exists + traceFile = new File(traceFilesDir, SentryUUID.generateSentryId() + ".trace"); + + measurementsMap.clear(); + screenFrameRateMeasurements.clear(); + slowFrameRenderMeasurements.clear(); + frozenFrameRenderMeasurements.clear(); + + frameMetricsCollectorId = + frameMetricsCollector.startCollection( + new SentryFrameMetricsCollector.FrameMetricsCollectorListener() { + float lastRefreshRate = 0; + + @Override + public void onFrameMetricCollected( + final long frameStartNanos, + final long frameEndNanos, + final long durationNanos, + final long delayNanos, + final boolean isSlow, + final boolean isFrozen, + final float refreshRate) { + // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), + // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp + // relative to profileStartNanos + final long frameTimestampRelativeNanos = + frameEndNanos + - System.nanoTime() + + SystemClock.elapsedRealtimeNanos() + - profileStartNanos; + + // We don't allow negative relative timestamps. + // So we add a check, even if this should never happen. + if (frameTimestampRelativeNanos < 0) { + return; + } + if (isFrozen) { + frozenFrameRenderMeasurements.addLast( + new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + } else if (isSlow) { + slowFrameRenderMeasurements.addLast( + new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + } + if (refreshRate != lastRefreshRate) { + lastRefreshRate = refreshRate; + screenFrameRateMeasurements.addLast( + new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate)); + } } - if (refreshRate != lastRefreshRate) { - lastRefreshRate = refreshRate; - screenFrameRateMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate)); - } - } - }); - - // We stop profiling after a timeout to avoid huge profiles to be sent - try { - scheduledFinish = - executorService.schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); - } catch (RejectedExecutionException e) { - logger.log( - SentryLevel.ERROR, - "Failed to call the executor. Profiling will not be automatically finished. Did you call Sentry.close()?", - e); - } - - profileStartNanos = SystemClock.elapsedRealtimeNanos(); - final @NotNull Date profileStartTimestamp = DateUtils.getCurrentDateTime(); - long profileStartCpuMillis = Process.getElapsedCpuTime(); + }); + + // We stop profiling after a timeout to avoid huge profiles to be sent + try { + scheduledFinish = + executorService.schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + } catch (RejectedExecutionException e) { + logger.log( + SentryLevel.ERROR, + "Failed to call the executor. Profiling will not be automatically finished. Did you call Sentry.close()?", + e); + } - // We don't make any check on the file existence or writeable state, because we don't want to - // make file IO in the main thread. - // We cannot offload the work to the executorService, as if that's very busy, profiles could - // start/stop with a lot of delay and even cause ANRs. - try { - // If there is any problem with the file this method will throw (but it will not throw in - // tests) - Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); - isRunning = true; - return new ProfileStartData(profileStartNanos, profileStartCpuMillis, profileStartTimestamp); - } catch (Throwable e) { - endAndCollect(false, null); - logger.log(SentryLevel.ERROR, "Unable to start a profile: ", e); - isRunning = false; - return null; + profileStartNanos = SystemClock.elapsedRealtimeNanos(); + final @NotNull Date profileStartTimestamp = DateUtils.getCurrentDateTime(); + long profileStartCpuMillis = Process.getElapsedCpuTime(); + + // We don't make any check on the file existence or writeable state, because we don't want to + // make file IO in the main thread. + // We cannot offload the work to the executorService, as if that's very busy, profiles could + // start/stop with a lot of delay and even cause ANRs. + try { + // If there is any problem with the file this method will throw (but it will not throw in + // tests) + Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); + isRunning = true; + return new ProfileStartData( + profileStartNanos, profileStartCpuMillis, profileStartTimestamp); + } catch (Throwable e) { + endAndCollect(false, null); + logger.log(SentryLevel.ERROR, "Unable to start a profile: ", e); + isRunning = false; + return null; + } } } @SuppressLint("NewApi") - public synchronized @Nullable ProfileEndData endAndCollect( + public @Nullable ProfileEndData endAndCollect( final boolean isTimeout, final @Nullable List performanceCollectionData) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!isRunning) { + logger.log(SentryLevel.WARNING, "Profiler not running"); + return null; + } - if (!isRunning) { - logger.log(SentryLevel.WARNING, "Profiler not running"); - return null; - } + try { + // If there is any problem with the file this method could throw, but the start is also + // wrapped, so this should never happen (except for tests, where this is the only method + // that + // throws) + Debug.stopMethodTracing(); + } catch (Throwable e) { + logger.log(SentryLevel.ERROR, "Error while stopping profiling: ", e); + } finally { + isRunning = false; + } + frameMetricsCollector.stopCollection(frameMetricsCollectorId); - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return null; + long transactionEndNanos = SystemClock.elapsedRealtimeNanos(); + long transactionEndCpuMillis = Process.getElapsedCpuTime(); - try { - // If there is any problem with the file this method could throw, but the start is also - // wrapped, so this should never happen (except for tests, where this is the only method that - // throws) - Debug.stopMethodTracing(); - } catch (Throwable e) { - logger.log(SentryLevel.ERROR, "Error while stopping profiling: ", e); - } finally { - isRunning = false; - } - frameMetricsCollector.stopCollection(frameMetricsCollectorId); - - long transactionEndNanos = SystemClock.elapsedRealtimeNanos(); - long transactionEndCpuMillis = Process.getElapsedCpuTime(); + if (traceFile == null) { + logger.log(SentryLevel.ERROR, "Trace file does not exists"); + return null; + } - if (traceFile == null) { - logger.log(SentryLevel.ERROR, "Trace file does not exists"); - return null; - } + if (!slowFrameRenderMeasurements.isEmpty()) { + measurementsMap.put( + ProfileMeasurement.ID_SLOW_FRAME_RENDERS, + new ProfileMeasurement( + ProfileMeasurement.UNIT_NANOSECONDS, slowFrameRenderMeasurements)); + } + if (!frozenFrameRenderMeasurements.isEmpty()) { + measurementsMap.put( + ProfileMeasurement.ID_FROZEN_FRAME_RENDERS, + new ProfileMeasurement( + ProfileMeasurement.UNIT_NANOSECONDS, frozenFrameRenderMeasurements)); + } + if (!screenFrameRateMeasurements.isEmpty()) { + measurementsMap.put( + ProfileMeasurement.ID_SCREEN_FRAME_RATES, + new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements)); + } + putPerformanceCollectionDataInMeasurements(performanceCollectionData); - if (!slowFrameRenderMeasurements.isEmpty()) { - measurementsMap.put( - ProfileMeasurement.ID_SLOW_FRAME_RENDERS, - new ProfileMeasurement(ProfileMeasurement.UNIT_NANOSECONDS, slowFrameRenderMeasurements)); - } - if (!frozenFrameRenderMeasurements.isEmpty()) { - measurementsMap.put( - ProfileMeasurement.ID_FROZEN_FRAME_RENDERS, - new ProfileMeasurement( - ProfileMeasurement.UNIT_NANOSECONDS, frozenFrameRenderMeasurements)); - } - if (!screenFrameRateMeasurements.isEmpty()) { - measurementsMap.put( - ProfileMeasurement.ID_SCREEN_FRAME_RATES, - new ProfileMeasurement(ProfileMeasurement.UNIT_HZ, screenFrameRateMeasurements)); - } - putPerformanceCollectionDataInMeasurements(performanceCollectionData); + if (scheduledFinish != null) { + scheduledFinish.cancel(true); + scheduledFinish = null; + } - if (scheduledFinish != null) { - scheduledFinish.cancel(true); - scheduledFinish = null; + return new ProfileEndData( + transactionEndNanos, transactionEndCpuMillis, isTimeout, traceFile, measurementsMap); } - - return new ProfileEndData( - transactionEndNanos, transactionEndCpuMillis, isTimeout, traceFile, measurementsMap); } - public synchronized void close() { - // we cancel any scheduled work - if (scheduledFinish != null) { - scheduledFinish.cancel(true); - scheduledFinish = null; - } + public void close() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // we cancel any scheduled work + if (scheduledFinish != null) { + scheduledFinish.cancel(true); + scheduledFinish = null; + } - // stop profiling if running - if (isRunning) { - endAndCollect(true, null); + // stop profiling if running + if (isRunning) { + endAndCollect(true, null); + } } } @@ -293,12 +293,6 @@ public synchronized void close() { private void putPerformanceCollectionDataInMeasurements( final @Nullable List performanceCollectionData) { - // onTransactionStart() is only available since Lollipop - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - return; - } - // This difference is required, since the PerformanceCollectionData timestamps are expressed in // terms of System.currentTimeMillis() and measurements timestamps require the nanoseconds since // the beginning, expressed with SystemClock.elapsedRealtimeNanos() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index 41e57a886a4..3455ff8b8d5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -10,19 +10,20 @@ import android.os.Process; import android.os.SystemClock; import io.sentry.DateUtils; -import io.sentry.HubAdapter; -import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; import io.sentry.ITransaction; import io.sentry.ITransactionProfiler; import io.sentry.PerformanceCollectionData; import io.sentry.ProfilingTraceData; import io.sentry.ProfilingTransactionData; +import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.Date; @@ -47,20 +48,7 @@ final class AndroidTransactionProfiler implements ITransactionProfiler { private long profileStartNanos; private long profileStartCpuMillis; private @NotNull Date profileStartTimestamp; - - /** - * @deprecated please use a constructor that doesn't takes a {@link IHub} instead, as it would be - * ignored anyway. - */ - @Deprecated - public AndroidTransactionProfiler( - final @NotNull Context context, - final @NotNull SentryAndroidOptions sentryAndroidOptions, - final @NotNull BuildInfoProvider buildInfoProvider, - final @NotNull SentryFrameMetricsCollector frameMetricsCollector, - final @NotNull IHub hub) { - this(context, sentryAndroidOptions, buildInfoProvider, frameMetricsCollector); - } + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public AndroidTransactionProfiler( final @NotNull Context context, @@ -133,27 +121,28 @@ private void init() { (int) SECONDS.toMicros(1) / profilingTracesHz, frameMetricsCollector, executorService, - logger, - buildInfoProvider); + logger); } @Override - public synchronized void start() { - // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler - // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; - - // Let's initialize trace folder and profiling interval - init(); - - transactionsCounter++; - // When the first transaction is starting, we can start profiling - if (transactionsCounter == 1 && onFirstStart()) { - logger.log(SentryLevel.DEBUG, "Profiler started."); - } else { - transactionsCounter--; - logger.log( - SentryLevel.WARNING, "A profile is already running. This profile will be ignored."); + public void start() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler + // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; + + // Let's initialize trace folder and profiling interval + init(); + + transactionsCounter++; + // When the first transaction is starting, we can start profiling + if (transactionsCounter == 1 && onFirstStart()) { + logger.log(SentryLevel.DEBUG, "Profiler started."); + } else { + transactionsCounter--; + logger.log( + SentryLevel.WARNING, "A profile is already running. This profile will be ignored."); + } } } @@ -176,133 +165,138 @@ private boolean onFirstStart() { } @Override - public synchronized void bindTransaction(final @NotNull ITransaction transaction) { - // If the profiler is running, but no profilingTransactionData is set, we bind it here - if (transactionsCounter > 0 && currentProfilingTransactionData == null) { - currentProfilingTransactionData = - new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis); + public void bindTransaction(final @NotNull ITransaction transaction) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // If the profiler is running, but no profilingTransactionData is set, we bind it here + if (transactionsCounter > 0 && currentProfilingTransactionData == null) { + currentProfilingTransactionData = + new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis); + } } } @Override - public @Nullable synchronized ProfilingTraceData onTransactionFinish( + public @Nullable ProfilingTraceData onTransactionFinish( final @NotNull ITransaction transaction, final @Nullable List performanceCollectionData, final @NotNull SentryOptions options) { - - return onTransactionFinish( - transaction.getName(), - transaction.getEventId().toString(), - transaction.getSpanContext().getTraceId().toString(), - false, - performanceCollectionData, - options); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + return onTransactionFinish( + transaction.getName(), + transaction.getEventId().toString(), + transaction.getSpanContext().getTraceId().toString(), + false, + performanceCollectionData, + options); + } } @SuppressLint("NewApi") - private @Nullable synchronized ProfilingTraceData onTransactionFinish( + private @Nullable ProfilingTraceData onTransactionFinish( final @NotNull String transactionName, final @NotNull String transactionId, final @NotNull String traceId, final boolean isTimeout, final @Nullable List performanceCollectionData, final @NotNull SentryOptions options) { - // check if profiler was created - if (profiler == null) { - return null; - } - - // onTransactionStart() is only available since Lollipop_MR1 - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; - - // Transaction finished, but it's not in the current profile - if (currentProfilingTransactionData == null - || !currentProfilingTransactionData.getId().equals(transactionId)) { - // A transaction is finishing, but it's not profiled. We can skip it - logger.log( - SentryLevel.INFO, - "Transaction %s (%s) finished, but was not currently being profiled. Skipping", - transactionName, - traceId); - return null; - } + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + // check if profiler was created + if (profiler == null) { + return null; + } - if (transactionsCounter > 0) { - transactionsCounter--; - } + // onTransactionStart() is only available since Lollipop_MR1 + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; + + // Transaction finished, but it's not in the current profile + if (currentProfilingTransactionData == null + || !currentProfilingTransactionData.getId().equals(transactionId)) { + // A transaction is finishing, but it's not profiled. We can skip it + logger.log( + SentryLevel.INFO, + "Transaction %s (%s) finished, but was not currently being profiled. Skipping", + transactionName, + traceId); + return null; + } - logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId); + if (transactionsCounter > 0) { + transactionsCounter--; + } - if (transactionsCounter != 0) { - // We notify the data referring to this transaction that it finished - if (currentProfilingTransactionData != null) { - currentProfilingTransactionData.notifyFinish( - SystemClock.elapsedRealtimeNanos(), - profileStartNanos, - Process.getElapsedCpuTime(), - profileStartCpuMillis); + logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId); + + if (transactionsCounter != 0) { + // We notify the data referring to this transaction that it finished + if (currentProfilingTransactionData != null) { + currentProfilingTransactionData.notifyFinish( + SystemClock.elapsedRealtimeNanos(), + profileStartNanos, + Process.getElapsedCpuTime(), + profileStartCpuMillis); + } + return null; } - return null; - } - final AndroidProfiler.ProfileEndData endData = - profiler.endAndCollect(false, performanceCollectionData); - // check if profiler end successfully - if (endData == null) { - return null; - } + final AndroidProfiler.ProfileEndData endData = + profiler.endAndCollect(false, performanceCollectionData); + // check if profiler end successfully + if (endData == null) { + return null; + } - long transactionDurationNanos = endData.endNanos - profileStartNanos; + long transactionDurationNanos = endData.endNanos - profileStartNanos; - List transactionList = new ArrayList<>(1); - final ProfilingTransactionData txData = currentProfilingTransactionData; - if (txData != null) { - transactionList.add(txData); - } - currentProfilingTransactionData = null; - // We clear the counter in case of a timeout - transactionsCounter = 0; + List transactionList = new ArrayList<>(1); + final ProfilingTransactionData txData = currentProfilingTransactionData; + if (txData != null) { + transactionList.add(txData); + } + currentProfilingTransactionData = null; + // We clear the counter in case of a timeout + transactionsCounter = 0; + + String totalMem = "0"; + ActivityManager.MemoryInfo memInfo = getMemInfo(); + if (memInfo != null) { + totalMem = Long.toString(memInfo.totalMem); + } + String[] abis = Build.SUPPORTED_ABIS; - String totalMem = "0"; - ActivityManager.MemoryInfo memInfo = getMemInfo(); - if (memInfo != null) { - totalMem = Long.toString(memInfo.totalMem); - } - String[] abis = Build.SUPPORTED_ABIS; + // We notify all transactions data that all transactions finished. + // Some may not have been really finished, in case of a timeout + for (ProfilingTransactionData t : transactionList) { + t.notifyFinish( + endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis); + } - // We notify all transactions data that all transactions finished. - // Some may not have been really finished, in case of a timeout - for (ProfilingTransactionData t : transactionList) { - t.notifyFinish( - endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis); + // cpu max frequencies are read with a lambda because reading files is involved, so it will be + // done in the background when the trace file is read + return new ProfilingTraceData( + endData.traceFile, + profileStartTimestamp, + transactionList, + transactionName, + transactionId, + traceId, + Long.toString(transactionDurationNanos), + buildInfoProvider.getSdkInfoVersion(), + abis != null && abis.length > 0 ? abis[0] : "", + () -> CpuInfoUtils.getInstance().readMaxFrequencies(), + buildInfoProvider.getManufacturer(), + buildInfoProvider.getModel(), + buildInfoProvider.getVersionRelease(), + buildInfoProvider.isEmulator(), + totalMem, + options.getProguardUuid(), + options.getRelease(), + options.getEnvironment(), + (endData.didTimeout || isTimeout) + ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT + : ProfilingTraceData.TRUNCATION_REASON_NORMAL, + endData.measurementsMap); } - - // cpu max frequencies are read with a lambda because reading files is involved, so it will be - // done in the background when the trace file is read - return new ProfilingTraceData( - endData.traceFile, - profileStartTimestamp, - transactionList, - transactionName, - transactionId, - traceId, - Long.toString(transactionDurationNanos), - buildInfoProvider.getSdkInfoVersion(), - abis != null && abis.length > 0 ? abis[0] : "", - () -> CpuInfoUtils.getInstance().readMaxFrequencies(), - buildInfoProvider.getManufacturer(), - buildInfoProvider.getModel(), - buildInfoProvider.getVersionRelease(), - buildInfoProvider.isEmulator(), - totalMem, - options.getProguardUuid(), - options.getRelease(), - options.getEnvironment(), - (endData.didTimeout || isTimeout) - ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT - : ProfilingTraceData.TRUNCATION_REASON_NORMAL, - endData.measurementsMap); } @Override @@ -320,7 +314,7 @@ public void close() { currentProfilingTransactionData.getTraceId(), true, null, - HubAdapter.getInstance().getOptions()); + ScopesAdapter.getInstance().getOptions()); } else if (transactionsCounter != 0) { // in case the app start profiling is running, and it's not bound to a transaction, we still // stop profiling, but we also have to manually update the counter. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index 14fac4753d8..8243493a50b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -5,7 +5,8 @@ import android.annotation.SuppressLint; import android.content.Context; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryEvent; import io.sentry.SentryLevel; @@ -14,6 +15,7 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Mechanism; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.Closeable; @@ -30,7 +32,7 @@ public final class AnrIntegration implements Integration, Closeable { private final @NotNull Context context; private boolean isClosed = false; - private final @NotNull Object startLock = new Object(); + private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock(); public AnrIntegration(final @NotNull Context context) { this.context = ContextUtils.getApplicationContext(context); @@ -45,15 +47,17 @@ public AnrIntegration(final @NotNull Context context) { private @Nullable SentryOptions options; - private static final @NotNull Object watchDogLock = new Object(); + protected static final @NotNull AutoClosableReentrantLock watchDogLock = + new AutoClosableReentrantLock(); @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "SentryOptions is required"); - register(hub, (SentryAndroidOptions) options); + register(scopes, (SentryAndroidOptions) options); } - private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { + private void register( + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { options .getLogger() .log(SentryLevel.DEBUG, "AnrIntegration enabled: %s", options.isAnrEnabled()); @@ -65,9 +69,9 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio .getExecutorService() .submit( () -> { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { if (!isClosed) { - startAnrWatchdog(hub, options); + startAnrWatchdog(scopes, options); } } }); @@ -80,8 +84,8 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio } private void startAnrWatchdog( - final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { - synchronized (watchDogLock) { + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { + try (final @NotNull ISentryLifecycleToken ignored = watchDogLock.acquire()) { if (anrWatchDog == null) { options .getLogger() @@ -94,7 +98,7 @@ private void startAnrWatchdog( new ANRWatchDog( options.getAnrTimeoutIntervalMillis(), options.isAnrReportInDebug(), - error -> reportANR(hub, options, error), + error -> reportANR(scopes, options, error), options.getLogger(), context); anrWatchDog.start(); @@ -106,7 +110,7 @@ private void startAnrWatchdog( @TestOnly void reportANR( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options, final @NotNull ApplicationNotResponding error) { options.getLogger().log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage()); @@ -122,7 +126,7 @@ void reportANR( final AnrHint anrHint = new AnrHint(isAppInBackground); final Hint hint = HintUtils.createWithTypeCheckHint(anrHint); - hub.captureEvent(event, hint); + scopes.captureEvent(event, hint); } private @NotNull Throwable buildAnrThrowable( @@ -150,10 +154,10 @@ ANRWatchDog getANRWatchDog() { @Override public void close() throws IOException { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { isClosed = true; } - synchronized (watchDogLock) { + try (final @NotNull ISentryLifecycleToken ignored = watchDogLock.acquire()) { if (anrWatchDog != null) { anrWatchDog.interrupt(); anrWatchDog = null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index e914029c30c..7295671e449 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -499,6 +499,11 @@ private void setOptionsTags(final @NotNull SentryBaseEvent event) { } // endregion + @Override + public @Nullable Long getOrder() { + return 12000L; + } + // region static values private void setStaticValues(final @NotNull SentryEvent event) { mergeUser(event); @@ -624,7 +629,7 @@ private void setDevice(final @NotNull SentryBaseEvent event) { device.setFamily(ContextUtils.getFamily(options.getLogger())); device.setModel(Build.MODEL); device.setModelId(Build.ID); - device.setArchs(ContextUtils.getArchitectures(buildInfoProvider)); + device.setArchs(ContextUtils.getArchitectures()); final ActivityManager.MemoryInfo memInfo = ContextUtils.getMemInfo(context, options.getLogger()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index c19c3aeac67..6b66106d3f4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -9,8 +9,8 @@ import io.sentry.Attachment; import io.sentry.DateUtils; import io.sentry.Hint; -import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryEvent; import io.sentry.SentryLevel; @@ -69,7 +69,7 @@ public AnrV2Integration(final @NotNull Context context) { @SuppressLint("NewApi") // we do the check in the AnrIntegrationFactory @Override - public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -90,7 +90,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { try { options .getExecutorService() - .submit(new AnrProcessor(context, hub, this.options, dateProvider)); + .submit(new AnrProcessor(context, scopes, this.options, dateProvider)); } catch (Throwable e) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); } @@ -109,17 +109,17 @@ public void close() throws IOException { static class AnrProcessor implements Runnable { private final @NotNull Context context; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @NotNull SentryAndroidOptions options; private final long threshold; AnrProcessor( final @NotNull Context context, - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options, final @NotNull ICurrentDateProvider dateProvider) { this.context = context; - this.hub = hub; + this.scopes = scopes; this.options = options; this.threshold = dateProvider.getCurrentTimeMillis() - NINETY_DAYS_THRESHOLD; } @@ -277,7 +277,7 @@ private void reportAsSentryEvent( } } - final @NotNull SentryId sentryId = hub.captureEvent(event, hint); + final @NotNull SentryId sentryId = scopes.captureEvent(event, hint); final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID); if (!isEventDropped) { // Block until the event is flushed to disk and the last_reported_anr marker is updated 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 e11bd5d3b9f..ade16a329ed 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 @@ -8,7 +8,7 @@ import android.content.res.Configuration; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -25,7 +25,7 @@ public final class AppComponentsBreadcrumbsIntegration implements Integration, Closeable, ComponentCallbacks2 { private final @NotNull Context context; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryAndroidOptions options; public AppComponentsBreadcrumbsIntegration(final @NotNull Context context) { @@ -34,8 +34,8 @@ public AppComponentsBreadcrumbsIntegration(final @NotNull Context context) { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - this.hub = Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -102,7 +102,7 @@ public void onTrimMemory(final int level) { } private void captureLowMemoryBreadcrumb(final long timeMs, final @Nullable Integer level) { - if (hub != null) { + if (scopes != null) { final Breadcrumb breadcrumb = new Breadcrumb(timeMs); if (level != null) { // only add breadcrumb if TRIM_MEMORY_BACKGROUND, TRIM_MEMORY_MODERATE or @@ -126,13 +126,13 @@ private void captureLowMemoryBreadcrumb(final long timeMs, final @Nullable Integ breadcrumb.setMessage("Low memory"); breadcrumb.setData("action", "LOW_MEMORY"); breadcrumb.setLevel(SentryLevel.WARNING); - hub.addBreadcrumb(breadcrumb); + scopes.addBreadcrumb(breadcrumb); } } private void captureConfigurationChangedBreadcrumb( final long timeMs, final @NotNull Configuration newConfig) { - if (hub != null) { + if (scopes != null) { final Device.DeviceOrientation deviceOrientation = DeviceOrientations.getOrientation(context.getResources().getConfiguration().orientation); @@ -152,7 +152,7 @@ private void captureConfigurationChangedBreadcrumb( final Hint hint = new Hint(); hint.set(ANDROID_CONFIGURATION, newConfig); - hub.addBreadcrumb(breadcrumb, hint); + scopes.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 f730f4bc76a..322f184a602 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 @@ -3,11 +3,11 @@ import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import androidx.lifecycle.ProcessLifecycleOwner; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; -import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -32,8 +32,8 @@ public AppLifecycleIntegration() { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -58,12 +58,12 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio try { Class.forName("androidx.lifecycle.DefaultLifecycleObserver"); Class.forName("androidx.lifecycle.ProcessLifecycleOwner"); - if (AndroidMainThreadChecker.getInstance().isMainThread()) { - addObserver(hub); + if (AndroidThreadChecker.getInstance().isMainThread()) { + addObserver(scopes); } else { // some versions of the androidx lifecycle-process require this to be executed on the main // thread. - handler.post(() -> addObserver(hub)); + handler.post(() -> addObserver(scopes)); } } catch (ClassNotFoundException e) { options @@ -80,7 +80,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio } } - private void addObserver(final @NotNull IHub hub) { + private void addObserver(final @NotNull IScopes scopes) { // this should never happen, check added to avoid warnings from NullAway if (this.options == null) { return; @@ -88,7 +88,7 @@ private void addObserver(final @NotNull IHub hub) { watcher = new LifecycleWatcher( - hub, + scopes, this.options.getSessionTrackingIntervalMillis(), this.options.isEnableAutoSessionTracking(), this.options.isEnableAppLifecycleBreadcrumbs()); @@ -127,7 +127,7 @@ public void close() throws IOException { if (watcher == null) { return; } - if (AndroidMainThreadChecker.getInstance().isMainThread()) { + if (AndroidThreadChecker.getInstance().isMainThread()) { removeObserver(); } else { // some versions of the androidx lifecycle-process require this to be executed on the main diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java index d47372c0c84..d9633aed540 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppState.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import io.sentry.ISentryLifecycleToken; +import io.sentry.util.AutoClosableReentrantLock; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -9,6 +11,7 @@ @ApiStatus.Internal public final class AppState { private static @NotNull AppState instance = new AppState(); + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); private AppState() {} @@ -27,7 +30,9 @@ void resetInstance() { return inBackground; } - synchronized void setInBackground(final boolean inBackground) { - this.inBackground = inBackground; + void setInBackground(final boolean inBackground) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + this.inBackground = inBackground; + } } } 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 89fe856631b..7f3889e1436 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 @@ -293,16 +293,8 @@ public static boolean isForegroundImportance() { return Settings.Global.getString(context.getContentResolver(), "device_name"); } - @SuppressWarnings("deprecation") - @SuppressLint("NewApi") // we're wrapping into if-check with sdk version - static @NotNull String[] getArchitectures(final @NotNull BuildInfoProvider buildInfoProvider) { - final String[] supportedAbis; - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.LOLLIPOP) { - supportedAbis = Build.SUPPORTED_ABIS; - } else { - supportedAbis = new String[] {Build.CPU_ABI, Build.CPU_ABI2}; - } - return supportedAbis; + static @NotNull String[] getArchitectures() { + return Build.SUPPORTED_ABIS; } /** diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java index b4c5f1ed027..0b618636d32 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java @@ -4,7 +4,7 @@ import android.app.Application; import android.os.Bundle; import androidx.annotation.NonNull; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryOptions; import io.sentry.util.Objects; @@ -25,7 +25,7 @@ public CurrentActivityIntegration(final @NotNull Application application) { } @Override - public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { application.registerActivityLifecycleCallbacks(this); } 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 a2833d2b346..ff85162a38f 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 @@ -11,7 +11,7 @@ import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.SentryReplayEvent; -import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.App; @@ -216,7 +216,7 @@ private void setThreads(final @NotNull SentryEvent event, final @NotNull Hint hi final boolean isHybridSDK = HintUtils.isFromHybridSdk(hint); for (final SentryThread thread : event.getThreads()) { - final boolean isMainThread = AndroidMainThreadChecker.getInstance().isMainThread(thread); + final boolean isMainThread = AndroidThreadChecker.getInstance().isMainThread(thread); // TODO: Fix https://github.com/getsentry/team-mobile/issues/47 if (thread.isCurrent() == null) { @@ -319,4 +319,9 @@ private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { return event; } + + @Override + public @Nullable Long getOrder() { + return 8000L; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index e2dfee2705a..25aad2d08c4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -15,6 +15,7 @@ import android.os.SystemClock; import android.util.DisplayMetrics; import io.sentry.DateUtils; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.CpuInfoUtils; @@ -22,6 +23,7 @@ import io.sentry.android.core.internal.util.RootChecker; import io.sentry.protocol.Device; import io.sentry.protocol.OperatingSystem; +import io.sentry.util.AutoClosableReentrantLock; import java.io.File; import java.util.Calendar; import java.util.Collections; @@ -40,6 +42,9 @@ public final class DeviceInfoUtil { @SuppressLint("StaticFieldLeak") private static volatile DeviceInfoUtil instance; + private static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); + private final @NotNull Context context; private final @NotNull SentryAndroidOptions options; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -74,7 +79,7 @@ public DeviceInfoUtil( public static DeviceInfoUtil getInstance( final @NotNull Context context, final @NotNull SentryAndroidOptions options) { if (instance == null) { - synchronized (DeviceInfoUtil.class) { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { if (instance == null) { instance = new DeviceInfoUtil(ContextUtils.getApplicationContext(context), options); } @@ -104,7 +109,7 @@ public Device collectDeviceInformation( device.setFamily(ContextUtils.getFamily(options.getLogger())); device.setModel(Build.MODEL); device.setModelId(Build.ID); - device.setArchs(ContextUtils.getArchitectures(buildInfoProvider)); + device.setArchs(ContextUtils.getArchitectures()); device.setOrientation(getOrientation()); if (isEmulator != null) { @@ -128,9 +133,6 @@ public Device collectDeviceInformation( } final @NotNull Locale locale = Locale.getDefault(); - if (device.getLanguage() == null) { - device.setLanguage(locale.getLanguage()); - } if (device.getLocale() == null) { device.setLocale(locale.toString()); // eg en_US } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java index f99294584b8..a921f794588 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java @@ -1,11 +1,13 @@ package io.sentry.android.core; -import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.OutboxSender; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.Closeable; import org.jetbrains.annotations.NotNull; @@ -17,15 +19,15 @@ public abstract class EnvelopeFileObserverIntegration implements Integration, Cl private @Nullable EnvelopeFileObserver observer; private @Nullable ILogger logger; private boolean isClosed = false; - private final @NotNull Object startLock = new Object(); + protected final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock(); public static @NotNull EnvelopeFileObserverIntegration getOutboxFileObserver() { return new OutboxEnvelopeFileObserverIntegration(); } @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); Objects.requireNonNull(options, "SentryOptions is required"); logger = options.getLogger(); @@ -44,9 +46,9 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions .getExecutorService() .submit( () -> { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { if (!isClosed) { - startOutboxSender(hub, options, path); + startOutboxSender(scopes, options, path); } } }); @@ -60,10 +62,12 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions } private void startOutboxSender( - final @NotNull IHub hub, final @NotNull SentryOptions options, final @NotNull String path) { + final @NotNull IScopes scopes, + final @NotNull SentryOptions options, + final @NotNull String path) { final OutboxSender outboxSender = new OutboxSender( - hub, + scopes, options.getEnvelopeReader(), options.getSerializer(), options.getLogger(), @@ -86,7 +90,7 @@ private void startOutboxSender( @Override public void close() { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { isClosed = true; } if (observer != null) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java index 007bb306cdd..ba08e71342a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/Installation.java @@ -1,13 +1,15 @@ package io.sentry.android.core; import android.content.Context; +import io.sentry.ISentryLifecycleToken; +import io.sentry.SentryUUID; +import io.sentry.util.AutoClosableReentrantLock; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.charset.Charset; -import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -19,6 +21,9 @@ final class Installation { private static final Charset UTF_8 = Charset.forName("UTF-8"); + protected static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); + private Installation() {} /** @@ -29,20 +34,22 @@ private Installation() {} * @return the generated installationId * @throws RuntimeException if not possible to read nor to write to the file. */ - public static synchronized String id(final @NotNull Context context) throws RuntimeException { - if (deviceId == null) { - final File installation = new File(context.getFilesDir(), INSTALLATION); - try { - if (!installation.exists()) { - deviceId = writeInstallationFile(installation); - return deviceId; + public static String id(final @NotNull Context context) throws RuntimeException { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { + if (deviceId == null) { + final File installation = new File(context.getFilesDir(), INSTALLATION); + try { + if (!installation.exists()) { + deviceId = writeInstallationFile(installation); + return deviceId; + } + deviceId = readInstallationFile(installation); + } catch (Throwable e) { + throw new RuntimeException(e); } - deviceId = readInstallationFile(installation); - } catch (Throwable e) { - throw new RuntimeException(e); } + return deviceId; } - return deviceId; } @TestOnly @@ -58,7 +65,7 @@ public static synchronized String id(final @NotNull Context context) throws Runt static @NotNull String writeInstallationFile(final @NotNull File installation) throws IOException { try (final OutputStream out = new FileOutputStream(installation)) { - final String id = UUID.randomUUID().toString(); + final String id = SentryUUID.generateSentryId(); out.write(id.getBytes(UTF_8)); out.flush(); return id; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index 4f2448d8d9d..de1af477b2f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -8,12 +8,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import io.sentry.DateUtils; -import io.sentry.HubAdapter; -import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.IScope; +import io.sentry.IScopes; import io.sentry.ISerializer; import io.sentry.ObjectWriter; +import io.sentry.ScopeType; +import io.sentry.ScopesAdapter; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; import io.sentry.SentryEvent; @@ -47,13 +48,14 @@ public final class InternalSentrySdk { /** - * @return a copy of the current hub's topmost scope, or null in case the hub is disabled + * @return a copy of the current scopes's topmost scope, or null in case the scopes is disabled */ @Nullable public static IScope getCurrentScope() { final @NotNull AtomicReference scopeRef = new AtomicReference<>(); - HubAdapter.getInstance() + ScopesAdapter.getInstance() .configureScope( + ScopeType.COMBINED, scope -> { scopeRef.set(scope.clone()); }); @@ -142,8 +144,8 @@ public static Map serializeScope( } /** - * Captures the provided envelope. Compared to {@link IHub#captureEvent(SentryEvent)} this method - *
+ * Captures the provided envelope. Compared to {@link IScopes#captureEvent(SentryEvent)} this + * method
* - will not enrich events with additional data (e.g. scope)
* - will not execute beforeSend: it's up to the caller to take care of this
* - will not perform any sampling: it's up to the caller to take care of this
@@ -156,8 +158,8 @@ public static Map serializeScope( @Nullable public static SentryId captureEnvelope( final @NotNull byte[] envelopeData, final boolean maybeStartNewSession) { - final @NotNull IHub hub = HubAdapter.getInstance(); - final @NotNull SentryOptions options = hub.getOptions(); + final @NotNull IScopes scopes = ScopesAdapter.getInstance(); + final @NotNull SentryOptions options = scopes.getOptions(); try (final InputStream envelopeInputStream = new ByteArrayInputStream(envelopeData)) { final @NotNull ISerializer serializer = options.getSerializer(); @@ -187,22 +189,22 @@ public static SentryId captureEnvelope( } // update session and add it to envelope if necessary - final @Nullable Session session = updateSession(hub, options, status, crashedOrErrored); + final @Nullable Session session = updateSession(scopes, options, status, crashedOrErrored); if (session != null) { final SentryEnvelopeItem sessionItem = SentryEnvelopeItem.fromSession(serializer, session); envelopeItems.add(sessionItem); deleteCurrentSessionFile( options, // should be sync if going to crash or already not a main thread - !maybeStartNewSession || !hub.getOptions().getMainThreadChecker().isMainThread()); + !maybeStartNewSession || !scopes.getOptions().getThreadChecker().isMainThread()); if (maybeStartNewSession) { - hub.startSession(); + scopes.startSession(); } } final SentryEnvelope repackagedEnvelope = new SentryEnvelope(envelope.getHeader(), envelopeItems); - return hub.captureEnvelope(repackagedEnvelope); + return scopes.captureEnvelope(repackagedEnvelope); } catch (Throwable t) { options.getLogger().log(SentryLevel.ERROR, "Failed to capture envelope", t); } @@ -237,7 +239,7 @@ public static Map getAppStartMeasurement() { private static void addTimeSpanToSerializedSpans(TimeSpan span, List> spans) { if (span.hasNotStarted()) { - HubAdapter.getInstance() + ScopesAdapter.getInstance() .getOptions() .getLogger() .log(WARNING, "Can not convert not-started TimeSpan to Map for Hybrid SDKs."); @@ -245,7 +247,7 @@ private static void addTimeSpanToSerializedSpans(TimeSpan span, List sessionRef = new AtomicReference<>(); - hub.configureScope( + scopes.configureScope( scope -> { final @Nullable Session session = scope.getSession(); if (session != null) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java index 23072265eb0..73c37dfe973 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java @@ -3,11 +3,13 @@ import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import io.sentry.Breadcrumb; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.Session; import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; +import io.sentry.util.AutoClosableReentrantLock; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicBoolean; @@ -25,20 +27,20 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { private @Nullable TimerTask timerTask; private final @NotNull Timer timer = new Timer(true); - private final @NotNull Object timerLock = new Object(); - private final @NotNull IHub hub; + private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); + private final @NotNull IScopes scopes; private final boolean enableSessionTracking; private final boolean enableAppLifecycleBreadcrumbs; private final @NotNull ICurrentDateProvider currentDateProvider; LifecycleWatcher( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final long sessionIntervalMillis, final boolean enableSessionTracking, final boolean enableAppLifecycleBreadcrumbs) { this( - hub, + scopes, sessionIntervalMillis, enableSessionTracking, enableAppLifecycleBreadcrumbs, @@ -46,7 +48,7 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { } LifecycleWatcher( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final long sessionIntervalMillis, final boolean enableSessionTracking, final boolean enableAppLifecycleBreadcrumbs, @@ -54,7 +56,7 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { this.sessionIntervalMillis = sessionIntervalMillis; this.enableSessionTracking = enableSessionTracking; this.enableAppLifecycleBreadcrumbs = enableAppLifecycleBreadcrumbs; - this.hub = hub; + this.scopes = scopes; this.currentDateProvider = currentDateProvider; } @@ -74,7 +76,7 @@ private void startSession() { final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis(); - hub.configureScope( + scopes.configureScope( scope -> { if (lastUpdatedSession.get() == 0L) { final @Nullable Session currentSession = scope.getSession(); @@ -89,12 +91,12 @@ private void startSession() { if (lastUpdatedSession == 0L || (lastUpdatedSession + sessionIntervalMillis) <= currentTimeMillis) { if (enableSessionTracking) { - hub.startSession(); + scopes.startSession(); } - hub.getOptions().getReplayController().start(); + scopes.getOptions().getReplayController().start(); } else if (!isFreshSession.get()) { // only resume if it's not a fresh session, which has been started in SentryAndroid.init - hub.getOptions().getReplayController().resume(); + scopes.getOptions().getReplayController().resume(); } isFreshSession.set(false); this.lastUpdatedSession.set(currentTimeMillis); @@ -107,7 +109,7 @@ public void onStop(final @NotNull LifecycleOwner owner) { final long currentTimeMillis = currentDateProvider.getCurrentTimeMillis(); this.lastUpdatedSession.set(currentTimeMillis); - hub.getOptions().getReplayController().pause(); + scopes.getOptions().getReplayController().pause(); scheduleEndSession(); AppState.getInstance().setInBackground(true); @@ -115,7 +117,7 @@ public void onStop(final @NotNull LifecycleOwner owner) { } private void scheduleEndSession() { - synchronized (timerLock) { + try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { cancelTask(); if (timer != null) { timerTask = @@ -123,9 +125,9 @@ private void scheduleEndSession() { @Override public void run() { if (enableSessionTracking) { - hub.endSession(); + scopes.endSession(); } - hub.getOptions().getReplayController().stop(); + scopes.getOptions().getReplayController().stop(); } }; @@ -135,7 +137,7 @@ public void run() { } private void cancelTask() { - synchronized (timerLock) { + try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { if (timerTask != null) { timerTask.cancel(); timerTask = null; @@ -150,7 +152,7 @@ private void addAppBreadcrumb(final @NotNull String state) { breadcrumb.setData("state", state); breadcrumb.setCategory("app.lifecycle"); breadcrumb.setLevel(SentryLevel.INFO); - hub.addBreadcrumb(breadcrumb); + scopes.addBreadcrumb(breadcrumb); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java index 6401945cab2..34b8d1d5f19 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LoadClass.java @@ -1,13 +1,23 @@ package io.sentry.android.core; import io.sentry.ILogger; -import io.sentry.SentryLevel; import io.sentry.SentryOptions; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** An Adapter for making Class.forName testable */ -public final class LoadClass { +/** + * An Adapter for making Class.forName testable + * + * @deprecated please use {@link io.sentry.util.LoadClass} instead. + */ +@Deprecated +public final class LoadClass extends io.sentry.util.LoadClass { + + private final io.sentry.util.LoadClass delegate; + + public LoadClass() { + delegate = new io.sentry.util.LoadClass(); + } /** * Try to load a class via reflection @@ -17,30 +27,15 @@ public final class LoadClass { * @return a Class if it's available, or null */ public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { - try { - return Class.forName(clazz); - } catch (ClassNotFoundException e) { - if (logger != null) { - logger.log(SentryLevel.DEBUG, "Class not available:" + clazz, e); - } - } catch (UnsatisfiedLinkError e) { - if (logger != null) { - logger.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); - } - } catch (Throwable e) { - if (logger != null) { - logger.log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); - } - } - return null; + return delegate.loadClass(clazz, logger); } public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) { - return loadClass(clazz, logger) != null; + return delegate.isClassAvailable(clazz, logger); } public boolean isClassAvailable( final @NotNull String clazz, final @Nullable SentryOptions options) { - return isClassAvailable(clazz, options != null ? options.getLogger() : null); + return delegate.isClassAvailable(clazz, options); } } 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 2d2df5700af..fcb755ec02a 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 @@ -5,6 +5,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import io.sentry.ILogger; +import io.sentry.InitPriority; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SdkVersion; @@ -37,9 +38,6 @@ final class ManifestMetadataReader { 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"; - static final String AUTO_SESSION_TRACKING_ENABLE = "io.sentry.auto-session-tracking.enable"; static final String SESSION_TRACKING_TIMEOUT_INTERVAL_MILLIS = "io.sentry.session-tracking.timeout-interval-millis"; @@ -56,7 +54,6 @@ final class ManifestMetadataReader { static final String UNCAUGHT_EXCEPTION_HANDLER_ENABLE = "io.sentry.uncaught-exception-handler.enable"; - @Deprecated static final String TRACING_ENABLE = "io.sentry.traces.enable"; static final String TRACES_SAMPLE_RATE = "io.sentry.traces.sample-rate"; static final String TRACES_ACTIVITY_ENABLE = "io.sentry.traces.activity.enable"; static final String TRACES_ACTIVITY_AUTO_FINISH_ENABLE = @@ -65,14 +62,9 @@ final class ManifestMetadataReader { static final String TTFD_ENABLE = "io.sentry.traces.time-to-full-display.enable"; - static final String TRACES_PROFILING_ENABLE = "io.sentry.traces.profiling.enable"; static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate"; @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; - - // TODO: remove in favor of TRACE_PROPAGATION_TARGETS - @Deprecated static final String TRACING_ORIGINS = "io.sentry.traces.tracing-origins"; - static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; static final String ATTACH_THREADS = "io.sentry.attach-threads"; @@ -102,10 +94,6 @@ final class ManifestMetadataReader { static final String ENABLE_SCOPE_PERSISTENCE = "io.sentry.enable-scope-persistence"; - static final String ENABLE_METRICS = "io.sentry.enable-metrics"; - - static final String MAX_BREADCRUMBS = "io.sentry.max-breadcrumbs"; - static final String REPLAYS_SESSION_SAMPLE_RATE = "io.sentry.session-replay.session-sample-rate"; static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.on-error-sample-rate"; @@ -114,6 +102,10 @@ final class ManifestMetadataReader { static final String REPLAYS_MASK_ALL_IMAGES = "io.sentry.session-replay.mask-all-images"; + static final String FORCE_INIT = "io.sentry.force-init"; + + static final String MAX_BREADCRUMBS = "io.sentry.max-breadcrumbs"; + /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -152,14 +144,13 @@ static void applyMetadata( options.setAnrEnabled(readBool(metadata, logger, ANR_ENABLE, options.isAnrEnabled())); - // deprecated - final boolean enableSessionTracking = - readBool( - metadata, logger, SESSION_TRACKING_ENABLE, options.isEnableAutoSessionTracking()); - // use enableAutoSessionTracking as fallback options.setEnableAutoSessionTracking( - readBool(metadata, logger, AUTO_SESSION_TRACKING_ENABLE, enableSessionTracking)); + readBool( + metadata, + logger, + AUTO_SESSION_TRACKING_ENABLE, + options.isEnableAutoSessionTracking())); if (options.getSampleRate() == null) { final Double sampleRate = readDouble(metadata, logger, SAMPLE_RATE); @@ -279,6 +270,13 @@ static void applyMetadata( options.setSendClientReports( readBool(metadata, logger, CLIENT_REPORTS_ENABLE, options.isSendClientReports())); + final boolean isAutoInitEnabled = readBool(metadata, logger, AUTO_INIT, true); + if (isAutoInitEnabled) { + options.setInitPriority(InitPriority.LOW); + } + + options.setForceInit(readBool(metadata, logger, FORCE_INIT, options.isForceInit())); + options.setCollectAdditionalContext( readBool( metadata, @@ -286,10 +284,6 @@ static void applyMetadata( COLLECT_ADDITIONAL_CONTEXT, options.isCollectAdditionalContext())); - if (options.getEnableTracing() == null) { - options.setEnableTracing(readBoolNullable(metadata, logger, TRACING_ENABLE, null)); - } - if (options.getTracesSampleRate() == null) { final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); if (tracesSampleRate != -1) { @@ -314,9 +308,6 @@ static void applyMetadata( TRACES_ACTIVITY_AUTO_FINISH_ENABLE, options.isEnableActivityLifecycleTracingAutoFinish())); - options.setProfilingEnabled( - readBool(metadata, logger, TRACES_PROFILING_ENABLE, options.isProfilingEnabled())); - if (options.getProfilesSampleRate() == null) { final Double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE); if (profilesSampleRate != -1) { @@ -339,15 +330,7 @@ static void applyMetadata( List tracePropagationTargets = readList(metadata, logger, TRACE_PROPAGATION_TARGETS); - // TODO remove once TRACING_ORIGINS have been removed - if (!metadata.containsKey(TRACE_PROPAGATION_TARGETS) - && (tracePropagationTargets == null || tracePropagationTargets.isEmpty())) { - tracePropagationTargets = readList(metadata, logger, TRACING_ORIGINS); - } - - if ((metadata.containsKey(TRACE_PROPAGATION_TARGETS) - || metadata.containsKey(TRACING_ORIGINS)) - && tracePropagationTargets == null) { + if (metadata.containsKey(TRACE_PROPAGATION_TARGETS) && tracePropagationTargets == null) { options.setTracePropagationTargets(Collections.emptyList()); } else if (tracePropagationTargets != null) { options.setTracePropagationTargets(tracePropagationTargets); @@ -396,9 +379,6 @@ static void applyMetadata( readBool( metadata, logger, ENABLE_SCOPE_PERSISTENCE, options.isEnableScopePersistence())); - options.setEnableMetrics( - readBool(metadata, logger, ENABLE_METRICS, options.isEnableMetrics())); - if (options.getSessionReplay().getSessionSampleRate() == null) { final Double sessionSampleRate = readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE); 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 78bcadeade2..8353a32d65d 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 @@ -2,7 +2,7 @@ import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -28,8 +28,8 @@ public NdkIntegration(final @Nullable Class sentryNdkClass) { } @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -38,7 +38,8 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions final boolean enabled = this.options.isEnableNdk(); this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration enabled: %s", enabled); - // Note: `hub` isn't used here because the NDK integration writes files to disk which are picked + // Note: `scopes` isn't used here because the NDK integration writes files to disk which are + // picked // up by another integration (EnvelopeFileObserverIntegration). if (enabled && sentryNdkClass != null) { final String cachedDir = this.options.getCacheDirPath(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index 7610a804f3b..5cfb5df2235 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -9,18 +9,19 @@ import android.net.NetworkCapabilities; import android.os.Build; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import io.sentry.Breadcrumb; import io.sentry.DateUtils; import io.sentry.Hint; -import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryDateProvider; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.TypeCheckHint; import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -33,7 +34,7 @@ public final class NetworkBreadcrumbsIntegration implements Integration, Closeab private final @NotNull Context context; private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull ILogger logger; - private final @NotNull Object lock = new Object(); + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); private volatile boolean isClosed; private @Nullable SentryOptions options; @@ -50,10 +51,9 @@ public NetworkBreadcrumbsIntegration( this.logger = Objects.requireNonNull(logger, "ILogger is required"); } - @SuppressLint("NewApi") @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); SentryAndroidOptions androidOptions = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -86,10 +86,10 @@ public void run() { return; } - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { networkCallback = new NetworkBreadcrumbsNetworkCallback( - hub, buildInfoProvider, options.getDateProvider()); + scopes, buildInfoProvider, options.getDateProvider()); final boolean registered = AndroidConnectionStatusProvider.registerNetworkCallback( @@ -120,10 +120,10 @@ public void close() throws IOException { .getExecutorService() .submit( () -> { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (networkCallback != null) { AndroidConnectionStatusProvider.unregisterNetworkCallback( - context, logger, buildInfoProvider, networkCallback); + context, logger, networkCallback); logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration removed."); } networkCallback = null; @@ -134,10 +134,8 @@ public void close() throws IOException { } } - @SuppressLint("ObsoleteSdkInt") - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) static final class NetworkBreadcrumbsNetworkCallback extends ConnectivityManager.NetworkCallback { - final @NotNull IHub hub; + final @NotNull IScopes scopes; final @NotNull BuildInfoProvider buildInfoProvider; @Nullable Network currentNetwork = null; @@ -147,10 +145,10 @@ static final class NetworkBreadcrumbsNetworkCallback extends ConnectivityManager final @NotNull SentryDateProvider dateProvider; NetworkBreadcrumbsNetworkCallback( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull SentryDateProvider dateProvider) { - this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.buildInfoProvider = Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); this.dateProvider = Objects.requireNonNull(dateProvider, "SentryDateProvider is required"); @@ -162,7 +160,7 @@ public void onAvailable(final @NonNull Network network) { return; } final Breadcrumb breadcrumb = createBreadcrumb("NETWORK_AVAILABLE"); - hub.addBreadcrumb(breadcrumb); + scopes.addBreadcrumb(breadcrumb); currentNetwork = network; lastCapabilities = null; } @@ -192,7 +190,7 @@ public void onCapabilitiesChanged( } Hint hint = new Hint(); hint.set(TypeCheckHint.ANDROID_NETWORK_CAPABILITIES, connectionDetail); - hub.addBreadcrumb(breadcrumb, hint); + scopes.addBreadcrumb(breadcrumb, hint); } @Override @@ -201,7 +199,7 @@ public void onLost(final @NonNull Network network) { return; } final Breadcrumb breadcrumb = createBreadcrumb("NETWORK_LOST"); - hub.addBreadcrumb(breadcrumb); + scopes.addBreadcrumb(breadcrumb); currentNetwork = null; lastCapabilities = null; } @@ -246,8 +244,7 @@ static class NetworkBreadcrumbConnectionDetail { final boolean isVpn; final @NotNull String type; - @SuppressLint({"NewApi", "ObsoleteSdkInt"}) - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @SuppressLint({"NewApi"}) NetworkBreadcrumbConnectionDetail( final @NotNull NetworkCapabilities networkCapabilities, final @NotNull BuildInfoProvider buildInfoProvider, @@ -264,7 +261,7 @@ static class NetworkBreadcrumbConnectionDetail { this.signalStrength = strength > -100 ? strength : 0; this.isVpn = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); String connectionType = - AndroidConnectionStatusProvider.getConnectionType(networkCapabilities, buildInfoProvider); + AndroidConnectionStatusProvider.getConnectionType(networkCapabilities); this.type = connectionType != null ? connectionType : ""; this.timestampNanos = capabilityNanos; } 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 b75b1e35c06..f7b51cce620 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 @@ -4,15 +4,16 @@ import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM; import static io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP; -import android.os.Looper; import io.sentry.EventProcessor; import io.sentry.Hint; +import io.sentry.ISentryLifecycleToken; import io.sentry.MeasurementUnit; import io.sentry.SentryEvent; import io.sentry.SpanContext; import io.sentry.SpanDataConvention; import io.sentry.SpanId; import io.sentry.SpanStatus; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.App; @@ -20,6 +21,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentrySpan; import io.sentry.protocol.SentryTransaction; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.util.HashMap; import java.util.List; @@ -43,6 +45,7 @@ final class PerformanceAndroidEventProcessor implements EventProcessor { private final @NotNull ActivityFramesTracker activityFramesTracker; private final @NotNull SentryAndroidOptions options; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); PerformanceAndroidEventProcessor( final @NotNull SentryAndroidOptions options, @@ -70,69 +73,72 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { @SuppressWarnings("NullAway") @Override - public synchronized @NotNull SentryTransaction process( + public @NotNull SentryTransaction process( @NotNull SentryTransaction transaction, @NotNull Hint hint) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (!options.isTracingEnabled()) { + return transaction; + } - if (!options.isTracingEnabled()) { - return transaction; - } + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + // the app start measurement is only sent once and only if the transaction has + // the app.start span, which is automatically created by the SDK. + if (hasAppStartSpan(transaction)) { + if (appStartMetrics.shouldSendStartMeasurements()) { + final @NotNull TimeSpan appStartTimeSpan = + appStartMetrics.getAppStartTimeSpanWithFallback(options); + final long appStartUpDurationMs = appStartTimeSpan.getDurationMs(); + + // if appStartUpDurationMs is 0, metrics are not ready to be sent + if (appStartUpDurationMs != 0) { + final MeasurementValue value = + new MeasurementValue( + (float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName()); + + final String appStartKey = + appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD + ? MeasurementValue.KEY_APP_START_COLD + : MeasurementValue.KEY_APP_START_WARM; + + transaction.getMeasurements().put(appStartKey, value); + + attachAppStartSpans(appStartMetrics, transaction); + appStartMetrics.onAppStartSpansSent(); + } + } - final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); - // the app start measurement is only sent once and only if the transaction has - // the app.start span, which is automatically created by the SDK. - if (hasAppStartSpan(transaction)) { - if (appStartMetrics.shouldSendStartMeasurements()) { - final @NotNull TimeSpan appStartTimeSpan = - appStartMetrics.getAppStartTimeSpanWithFallback(options); - final long appStartUpDurationMs = appStartTimeSpan.getDurationMs(); - - // if appStartUpDurationMs is 0, metrics are not ready to be sent - if (appStartUpDurationMs != 0) { - final MeasurementValue value = - new MeasurementValue( - (float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName()); - - final String appStartKey = - appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD - ? MeasurementValue.KEY_APP_START_COLD - : MeasurementValue.KEY_APP_START_WARM; - - transaction.getMeasurements().put(appStartKey, value); - - attachAppStartSpans(appStartMetrics, transaction); - appStartMetrics.onAppStartSpansSent(); + @Nullable App appContext = transaction.getContexts().getApp(); + if (appContext == null) { + appContext = new App(); + transaction.getContexts().setApp(appContext); } + final String appStartType = + appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD + ? "cold" + : "warm"; + appContext.setStartType(appStartType); } - @Nullable App appContext = transaction.getContexts().getApp(); - if (appContext == null) { - appContext = new App(); - transaction.getContexts().setApp(appContext); + setContributingFlags(transaction); + + final SentryId eventId = transaction.getEventId(); + final SpanContext spanContext = transaction.getContexts().getTrace(); + + // only add slow/frozen frames to transactions created by ActivityLifecycleIntegration + // which have the operation UI_LOAD_OP. If a user-defined (or hybrid SDK) transaction + // users it, we'll also add the metrics if available + if (eventId != null + && spanContext != null + && spanContext.getOperation().contentEquals(UI_LOAD_OP)) { + final Map framesMetrics = + activityFramesTracker.takeMetrics(eventId); + if (framesMetrics != null) { + transaction.getMeasurements().putAll(framesMetrics); + } } - final String appStartType = - appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD ? "cold" : "warm"; - appContext.setStartType(appStartType); - } - setContributingFlags(transaction); - - final SentryId eventId = transaction.getEventId(); - final SpanContext spanContext = transaction.getContexts().getTrace(); - - // only add slow/frozen frames to transactions created by ActivityLifecycleIntegration - // which have the operation UI_LOAD_OP. If a user-defined (or hybrid SDK) transaction - // users it, we'll also add the metrics if available - if (eventId != null - && spanContext != null - && spanContext.getOperation().contentEquals(UI_LOAD_OP)) { - final Map framesMetrics = - activityFramesTracker.takeMetrics(eventId); - if (framesMetrics != null) { - transaction.getMeasurements().putAll(framesMetrics); - } + return transaction; } - - return transaction; } private void setContributingFlags(SentryTransaction transaction) { @@ -278,7 +284,7 @@ private static SentrySpan timeSpanToSentrySpan( final @NotNull String operation) { final Map defaultSpanData = new HashMap<>(2); - defaultSpanData.put(SpanDataConvention.THREAD_ID, Looper.getMainLooper().getThread().getId()); + defaultSpanData.put(SpanDataConvention.THREAD_ID, AndroidThreadChecker.mainThreadSystemId); defaultSpanData.put(SpanDataConvention.THREAD_NAME, "main"); defaultSpanData.put(SpanDataConvention.CONTRIBUTES_TTID, true); @@ -296,7 +302,11 @@ private static SentrySpan timeSpanToSentrySpan( APP_METRICS_ORIGIN, new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), - null, defaultSpanData); } + + @Override + public @Nullable Long getOrder() { + return 9000L; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java index 249904fd162..a0111b3c0a8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java @@ -6,11 +6,13 @@ import android.content.Context; import android.telephony.TelephonyManager; import io.sentry.Breadcrumb; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.Permissions; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -25,7 +27,7 @@ public final class PhoneStateBreadcrumbsIntegration implements Integration, Clos @TestOnly @Nullable PhoneStateChangeListener listener; private @Nullable TelephonyManager telephonyManager; private boolean isClosed = false; - private final @NotNull Object startLock = new Object(); + private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock(); public PhoneStateBreadcrumbsIntegration(final @NotNull Context context) { this.context = @@ -33,8 +35,8 @@ public PhoneStateBreadcrumbsIntegration(final @NotNull Context context) { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -54,9 +56,9 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio .getExecutorService() .submit( () -> { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { if (!isClosed) { - startTelephonyListener(hub, options); + startTelephonyListener(scopes, options); } } }); @@ -73,11 +75,11 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio @SuppressWarnings("deprecation") private void startTelephonyListener( - final @NotNull IHub hub, final @NotNull SentryOptions options) { + final @NotNull IScopes scopes, final @NotNull SentryOptions options) { telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager != null) { try { - listener = new PhoneStateChangeListener(hub); + listener = new PhoneStateChangeListener(scopes); telephonyManager.listen(listener, android.telephony.PhoneStateListener.LISTEN_CALL_STATE); options.getLogger().log(SentryLevel.DEBUG, "PhoneStateBreadcrumbsIntegration installed."); @@ -95,7 +97,7 @@ private void startTelephonyListener( @SuppressWarnings("deprecation") @Override public void close() throws IOException { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { isClosed = true; } if (telephonyManager != null && listener != null) { @@ -111,10 +113,10 @@ public void close() throws IOException { @SuppressWarnings("deprecation") static final class PhoneStateChangeListener extends android.telephony.PhoneStateListener { - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; - PhoneStateChangeListener(final @NotNull IHub hub) { - this.hub = hub; + PhoneStateChangeListener(final @NotNull IScopes scopes) { + this.scopes = scopes; } @SuppressWarnings("deprecation") @@ -129,7 +131,7 @@ public void onCallStateChanged(int state, String incomingNumber) { breadcrumb.setData("action", "CALL_STATE_RINGING"); breadcrumb.setMessage("Device ringing"); breadcrumb.setLevel(SentryLevel.INFO); - hub.addBreadcrumb(breadcrumb); + scopes.addBreadcrumb(breadcrumb); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 8cdc2461d23..8585cb96142 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -89,7 +89,7 @@ public ScreenshotEventProcessor( final byte[] screenshot = takeScreenshot( - activity, options.getMainThreadChecker(), options.getLogger(), buildInfoProvider); + activity, options.getThreadChecker(), options.getLogger(), buildInfoProvider); if (screenshot == null) { return event; } @@ -98,4 +98,9 @@ public ScreenshotEventProcessor( hint.set(ANDROID_ACTIVITY, activity); return event; } + + @Override + public @Nullable Long getOrder() { + return 10000L; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java index 6d24508c122..41f4f838bf5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java @@ -4,12 +4,14 @@ import io.sentry.DataCategory; import io.sentry.IConnectionStatusProvider; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SendCachedEnvelopeFireAndForgetIntegration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.transport.RateLimiter; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.io.Closeable; @@ -30,11 +32,12 @@ final class SendCachedEnvelopeIntegration private final @NotNull LazyEvaluator startupCrashMarkerEvaluator; private final AtomicBoolean startupCrashHandled = new AtomicBoolean(false); private @Nullable IConnectionStatusProvider connectionStatusProvider; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryAndroidOptions options; private @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender; private final AtomicBoolean isInitialized = new AtomicBoolean(false); private final AtomicBoolean isClosed = new AtomicBoolean(false); + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public SendCachedEnvelopeIntegration( final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory, @@ -44,8 +47,8 @@ public SendCachedEnvelopeIntegration( } @Override - public void register(@NotNull IHub hub, @NotNull SentryOptions options) { - this.hub = Objects.requireNonNull(hub, "Hub is required"); + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -58,7 +61,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { } addIntegrationToSdkVersion("SendCachedEnvelope"); - sendCachedEnvelopes(hub, this.options); + sendCachedEnvelopes(scopes, this.options); } @Override @@ -72,15 +75,15 @@ public void close() throws IOException { @Override public void onConnectionStatusChanged( final @NotNull IConnectionStatusProvider.ConnectionStatus status) { - if (hub != null && options != null) { - sendCachedEnvelopes(hub, options); + if (scopes != null && options != null) { + sendCachedEnvelopes(scopes, options); } } @SuppressWarnings({"NullAway"}) - private synchronized void sendCachedEnvelopes( - final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { - try { + private void sendCachedEnvelopes( + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { final Future future = options .getExecutorService() @@ -100,7 +103,7 @@ private synchronized void sendCachedEnvelopes( connectionStatusProvider = options.getConnectionStatusProvider(); connectionStatusProvider.addConnectionStatusObserver(this); - sender = factory.create(hub, options); + sender = factory.create(scopes, options); } if (connectionStatusProvider != null @@ -113,7 +116,7 @@ private synchronized void sendCachedEnvelopes( } // in case there's rate limiting active, skip processing - final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index adeb451332a..ea1f8ae875c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -5,8 +5,9 @@ import android.content.Context; import android.os.Process; import android.os.SystemClock; -import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.OptionsContainer; import io.sentry.Sentry; @@ -17,6 +18,7 @@ import io.sentry.android.core.performance.TimeSpan; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; +import io.sentry.util.AutoClosableReentrantLock; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; @@ -43,6 +45,9 @@ public final class SentryAndroid { private static final String FRAGMENT_CLASS_NAME = "androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks"; + protected static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); + private SentryAndroid() {} /** @@ -84,16 +89,15 @@ public static void init( * @param configuration Sentry.OptionsConfiguration configuration handler */ @SuppressLint("NewApi") - public static synchronized void init( + public static void init( @NotNull final Context context, @NotNull ILogger logger, @NotNull Sentry.OptionsConfiguration configuration) { - - try { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { Sentry.init( OptionsContainer.create(SentryAndroidOptions.class), options -> { - final LoadClass classLoader = new LoadClass(); + final io.sentry.util.LoadClass classLoader = new io.sentry.util.LoadClass(); final boolean isTimberUpstreamAvailable = classLoader.isClassAvailable(TIMBER_CLASS_NAME, options); final boolean isFragmentUpstreamAvailable = @@ -109,7 +113,7 @@ public static synchronized void init( classLoader.isClassAvailable(SENTRY_REPLAY_INTEGRATION_CLASS_NAME, options); final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger); - final LoadClass loadClass = new LoadClass(); + final io.sentry.util.LoadClass loadClass = new io.sentry.util.LoadClass(); final ActivityFramesTracker activityFramesTracker = new ActivityFramesTracker(loadClass, options); @@ -167,14 +171,14 @@ public static synchronized void init( }, true); - final @NotNull IHub hub = Sentry.getCurrentHub(); + final @NotNull IScopes scopes = Sentry.getCurrentScopes(); if (ContextUtils.isForegroundImportance()) { - if (hub.getOptions().isEnableAutoSessionTracking()) { + if (scopes.getOptions().isEnableAutoSessionTracking()) { // The LifecycleWatcher of AppLifecycleIntegration may already started a session // so only start a session if it's not already started // This e.g. happens on React Native, or e.g. on deferred SDK init final AtomicBoolean sessionStarted = new AtomicBoolean(false); - hub.configureScope( + scopes.configureScope( scope -> { final @Nullable Session currentSession = scope.getSession(); if (currentSession != null && currentSession.getStarted() != null) { @@ -182,10 +186,10 @@ public static synchronized void init( } }); if (!sessionStarted.get()) { - hub.startSession(); + scopes.startSession(); } } - hub.getOptions().getReplayController().start(); + scopes.getOptions().getReplayController().start(); } } catch (IllegalAccessException e) { logger.log(SentryLevel.FATAL, "Fatal error during SentryAndroid.init(...)", e); 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 6ac168fd8fd..9c32920be89 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 @@ -54,8 +54,8 @@ public final class SentryAndroidOptions extends SentryOptions { * Enables the Auto instrumentation for Activity lifecycle tracing. * *
    - *
  • It also requires setting any of {@link SentryOptions#getEnableTracing()}, {@link - * SentryOptions#getTracesSampleRate()} or {@link SentryOptions#getTracesSampler()}. + *
  • It also requires setting any of {@link SentryOptions#getTracesSampleRate()} or {@link + * SentryOptions#getTracesSampler()}. *
* *
    @@ -209,7 +209,7 @@ public interface BeforeCaptureCallback { */ private boolean attachAnrThreadDump = false; - private boolean enablePerformanceV2 = false; + private boolean enablePerformanceV2 = true; private @Nullable SentryFrameMetricsCollector frameMetricsCollector; @@ -340,27 +340,6 @@ public void enableAllAutoBreadcrumbs(boolean enable) { setEnableUserInteractionBreadcrumbs(enable); } - /** - * Returns the interval for profiling traces in milliseconds. - * - * @return the interval for profiling traces in milliseconds. - * @deprecated has no effect and will be removed in future versions. It now just returns 0. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public int getProfilingTracesIntervalMillis() { - return 0; - } - - /** - * Sets the interval for profiling traces in milliseconds. - * - * @param profilingTracesIntervalMillis - the interval for profiling traces in milliseconds. - * @deprecated has no effect and will be removed in future versions. - */ - @Deprecated - public void setProfilingTracesIntervalMillis(final int profilingTracesIntervalMillis) {} - /** * Returns the Debug image loader * @@ -589,20 +568,18 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { * @return true if performance-v2 is enabled. See {@link #setEnablePerformanceV2(boolean)} for * more details. */ - @ApiStatus.Experimental public boolean isEnablePerformanceV2() { return enablePerformanceV2; } /** - * Experimental: Enables or disables the Performance V2 SDK features. + * Enables or disables the Performance V2 SDK features. * *

    With this change - Cold app start spans will provide more accurate timings - Cold app start * spans will be enriched with detailed ContentProvider, Application and Activity startup times * * @param enablePerformanceV2 true if enabled or false otherwise */ - @ApiStatus.Experimental public void setEnablePerformanceV2(final boolean enablePerformanceV2) { this.enablePerformanceV2 = enablePerformanceV2; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 8baf10ebc32..6658e145605 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -7,10 +7,10 @@ import android.content.Context; import android.content.pm.ProviderInfo; import android.net.Uri; -import android.os.Build; import android.os.Process; import android.os.SystemClock; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.JsonSerializer; import io.sentry.SentryAppStartProfilingOptions; @@ -21,6 +21,7 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; +import io.sentry.util.AutoClosableReentrantLock; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -43,6 +44,7 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider private final @NotNull ILogger logger; private final @NotNull BuildInfoProvider buildInfoProvider; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); @TestOnly SentryPerformanceProvider( @@ -82,7 +84,7 @@ public String getType(@NotNull Uri uri) { @Override public void shutdown() { - synchronized (AppStartMetrics.getInstance()) { + try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) { final @Nullable ITransactionProfiler appStartProfiler = AppStartMetrics.getInstance().getAppStartProfiler(); if (appStartProfiler != null) { @@ -99,11 +101,6 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } - // Debug.startMethodTracingSampling() is only available since Lollipop - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - return; - } - final @NotNull File cacheDir = AndroidOptionsInitializer.getCacheDir(context); final @NotNull File configFile = new File(cacheDir, APP_START_PROFILING_CONFIG_FILE_NAME); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java index b4279db13f7..a83454d29b7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java @@ -2,6 +2,7 @@ import io.sentry.DateUtils; import io.sentry.IPerformanceContinuousCollector; +import io.sentry.ISentryLifecycleToken; import io.sentry.ISpan; import io.sentry.ITransaction; import io.sentry.NoOpSpan; @@ -11,6 +12,7 @@ import io.sentry.SpanDataConvention; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.MeasurementValue; +import io.sentry.util.AutoClosableReentrantLock; import java.util.Date; import java.util.Iterator; import java.util.SortedSet; @@ -34,7 +36,7 @@ public class SpanFrameMetricsCollector private static final SentryNanotimeDate EMPTY_NANO_TIME = new SentryNanotimeDate(new Date(0), 0); private final boolean enabled; - private final @NotNull Object lock = new Object(); + protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; private volatile @Nullable String listenerId; @@ -43,17 +45,19 @@ public class SpanFrameMetricsCollector private final @NotNull SortedSet runningSpans = new TreeSet<>( (o1, o2) -> { + if (o1 == o2) { + return 0; + } int timeDiff = o1.getStartDate().compareTo(o2.getStartDate()); if (timeDiff != 0) { return timeDiff; - } else { - // TreeSet uses compareTo to check for duplicates, so ensure that - // two non-equal spans with the same start date are not considered equal - return o1.getSpanContext() - .getSpanId() - .toString() - .compareTo(o2.getSpanContext().getSpanId().toString()); } + // TreeSet uses compareTo to check for duplicates, so ensure that + // two non-equal spans with the same start date are not considered equal + return o1.getSpanContext() + .getSpanId() + .toString() + .compareTo(o2.getSpanContext().getSpanId().toString()); }); // all collected frames, sorted by frame end time @@ -85,7 +89,7 @@ public void onSpanStarted(final @NotNull ISpan span) { return; } - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { runningSpans.add(span); if (listenerId == null) { @@ -109,7 +113,7 @@ public void onSpanFinished(final @NotNull ISpan span) { } // ignore span if onSpanStarted was never called for it - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (!runningSpans.contains(span)) { return; } @@ -117,7 +121,7 @@ public void onSpanFinished(final @NotNull ISpan span) { captureFrameMetrics(span); - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (runningSpans.isEmpty()) { clear(); } else { @@ -130,7 +134,7 @@ public void onSpanFinished(final @NotNull ISpan span) { private void captureFrameMetrics(@NotNull final ISpan span) { // TODO lock still required? - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { boolean removed = runningSpans.remove(span); if (!removed) { return; @@ -224,7 +228,7 @@ private void captureFrameMetrics(@NotNull final ISpan span) { @Override public void clear() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (listenerId != null) { frameMetricsCollector.stopCollection(listenerId); listenerId = null; 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 ea838975cde..04988c9a98b 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 @@ -41,12 +41,14 @@ import android.os.Bundle; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.android.core.internal.util.Debouncer; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import io.sentry.util.StringUtils; import java.io.Closeable; @@ -69,7 +71,7 @@ public final class SystemEventsBreadcrumbsIntegration implements Integration, Cl private final @NotNull List actions; private boolean isClosed = false; - private final @NotNull Object startLock = new Object(); + private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock(); public SystemEventsBreadcrumbsIntegration(final @NotNull Context context) { this(context, getDefaultActions()); @@ -83,8 +85,8 @@ public SystemEventsBreadcrumbsIntegration( } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -104,9 +106,9 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio .getExecutorService() .submit( () -> { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { if (!isClosed) { - startSystemEventsReceiver(hub, (SentryAndroidOptions) options); + startSystemEventsReceiver(scopes, (SentryAndroidOptions) options); } } }); @@ -122,8 +124,8 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio } private void startSystemEventsReceiver( - final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { - receiver = new SystemEventsBroadcastReceiver(hub, options); + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { + receiver = new SystemEventsBroadcastReceiver(scopes, options); final IntentFilter filter = new IntentFilter(); for (String item : actions) { filter.addAction(item); @@ -193,7 +195,7 @@ private void startSystemEventsReceiver( @Override public void close() throws IOException { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { isClosed = true; } if (receiver != null) { @@ -209,14 +211,14 @@ public void close() throws IOException { static final class SystemEventsBroadcastReceiver extends BroadcastReceiver { private static final long DEBOUNCE_WAIT_TIME_MS = 60 * 1000; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @NotNull SentryAndroidOptions options; private final @NotNull Debouncer batteryChangedDebouncer = new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS, 0); SystemEventsBroadcastReceiver( - final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { - this.hub = hub; + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { + this.scopes = scopes; this.options = options; } @@ -240,7 +242,7 @@ public void onReceive(final Context context, final @NotNull Intent intent) { createBreadcrumb(now, intent, action, isBatteryChanged); final Hint hint = new Hint(); hint.set(ANDROID_INTENT, intent); - hub.addBreadcrumb(breadcrumb, hint); + scopes.addBreadcrumb(breadcrumb, hint); }); } catch (Throwable t) { options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java index b94a06b9768..1d72fa22396 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 @@ -11,10 +11,12 @@ import android.hardware.SensorManager; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -26,12 +28,12 @@ public final class TempSensorBreadcrumbsIntegration implements Integration, Closeable, SensorEventListener { private final @NotNull Context context; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryAndroidOptions options; @TestOnly @Nullable SensorManager sensorManager; private boolean isClosed = false; - private final @NotNull Object startLock = new Object(); + private final @NotNull AutoClosableReentrantLock startLock = new AutoClosableReentrantLock(); public TempSensorBreadcrumbsIntegration(final @NotNull Context context) { this.context = @@ -39,8 +41,8 @@ public TempSensorBreadcrumbsIntegration(final @NotNull Context context) { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - this.hub = Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, @@ -60,7 +62,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio .getExecutorService() .submit( () -> { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { if (!isClosed) { startSensorListener(options); } @@ -101,7 +103,7 @@ private void startSensorListener(final @NotNull SentryOptions options) { @Override public void close() throws IOException { - synchronized (startLock) { + try (final @NotNull ISentryLifecycleToken ignored = startLock.acquire()) { isClosed = true; } if (sensorManager != null) { @@ -122,7 +124,7 @@ public void onSensorChanged(final @NotNull SensorEvent event) { return; } - if (hub != null) { + if (scopes != null) { final Breadcrumb breadcrumb = new Breadcrumb(); breadcrumb.setType("system"); breadcrumb.setCategory("device.event"); @@ -135,7 +137,7 @@ public void onSensorChanged(final @NotNull SensorEvent event) { final Hint hint = new Hint(); hint.set(ANDROID_SENSOR_EVENT, event); - hub.addBreadcrumb(breadcrumb, hint); + scopes.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 a0ad3591669..46275504cbe 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 @@ -6,7 +6,7 @@ import android.app.Application; import android.os.Bundle; import android.view.Window; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -23,13 +23,13 @@ public final class UserInteractionIntegration implements Integration, Closeable, Application.ActivityLifecycleCallbacks { private final @NotNull Application application; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryAndroidOptions options; private final boolean isAndroidXAvailable; public UserInteractionIntegration( - final @NotNull Application application, final @NotNull LoadClass classLoader) { + final @NotNull Application application, final @NotNull io.sentry.util.LoadClass classLoader) { this.application = Objects.requireNonNull(application, "Application is required"); isAndroidXAvailable = classLoader.isClassAvailable("androidx.core.view.GestureDetectorCompat", options); @@ -44,14 +44,14 @@ private void startTracking(final @NotNull Activity activity) { return; } - if (hub != null && options != null) { + if (scopes != null && options != null) { Window.Callback delegate = window.getCallback(); if (delegate == null) { delegate = new NoOpWindowCallback(); } final SentryGestureListener gestureListener = - new SentryGestureListener(activity, hub, options); + new SentryGestureListener(activity, scopes, options); window.setCallback(new SentryWindowCallback(delegate, activity, gestureListener, options)); } } @@ -102,13 +102,13 @@ public void onActivitySaveInstanceState(@NotNull Activity activity, @NotNull Bun public void onActivityDestroyed(@NotNull Activity activity) {} @Override - public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, "SentryAndroidOptions is required"); - this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); final boolean integrationEnabled = this.options.isEnableUserInteractionBreadcrumbs() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index eaa9aaa5604..c32b05892f9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -15,7 +15,7 @@ import io.sentry.SentryLevel; import io.sentry.android.core.internal.gestures.ViewUtils; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; -import io.sentry.android.core.internal.util.AndroidMainThreadChecker; +import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.internal.util.ClassUtil; import io.sentry.android.core.internal.util.Debouncer; import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; @@ -25,7 +25,7 @@ import io.sentry.util.HintUtils; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; -import io.sentry.util.thread.IMainThreadChecker; +import io.sentry.util.thread.IThreadChecker; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -101,7 +101,7 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) snapshotViewHierarchy( activity, options.getViewHierarchyExporters(), - options.getMainThreadChecker(), + options.getThreadChecker(), options.getLogger()); if (viewHierarchy != null) { @@ -113,13 +113,13 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) public static byte[] snapshotViewHierarchyAsData( @Nullable Activity activity, - @NotNull IMainThreadChecker mainThreadChecker, + @NotNull IThreadChecker threadChecker, @NotNull ISerializer serializer, @NotNull ILogger logger) { @Nullable ViewHierarchy viewHierarchy = - snapshotViewHierarchy(activity, new ArrayList<>(0), mainThreadChecker, logger); + snapshotViewHierarchy(activity, new ArrayList<>(0), threadChecker, logger); if (viewHierarchy == null) { logger.log(SentryLevel.ERROR, "Could not get ViewHierarchy."); @@ -144,14 +144,14 @@ public static byte[] snapshotViewHierarchyAsData( public static ViewHierarchy snapshotViewHierarchy( final @Nullable Activity activity, final @NotNull ILogger logger) { return snapshotViewHierarchy( - activity, new ArrayList<>(0), AndroidMainThreadChecker.getInstance(), logger); + activity, new ArrayList<>(0), AndroidThreadChecker.getInstance(), logger); } @Nullable public static ViewHierarchy snapshotViewHierarchy( final @Nullable Activity activity, final @NotNull List exporters, - final @NotNull IMainThreadChecker mainThreadChecker, + final @NotNull IThreadChecker threadChecker, final @NotNull ILogger logger) { if (activity == null) { @@ -172,7 +172,7 @@ public static ViewHierarchy snapshotViewHierarchy( } try { - if (mainThreadChecker.isMainThread()) { + if (threadChecker.isMainThread()) { return snapshotViewHierarchy(decorView, exporters); } else { final CountDownLatch latch = new CountDownLatch(1); @@ -284,4 +284,9 @@ private static ViewHierarchyNode viewToNode(@NotNull final View view) { return node; } + + @Override + public @Nullable Long getOrder() { + return 11000L; + } } 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 0ec0d83258e..cd80f5ced7d 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 @@ -10,8 +10,8 @@ import android.view.Window; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; import io.sentry.IScope; +import io.sentry.IScopes; import io.sentry.ITransaction; import io.sentry.SentryLevel; import io.sentry.SpanStatus; @@ -43,7 +43,7 @@ private enum GestureType { private static final String TRACE_ORIGIN = "auto.ui.gesture_listener"; private final @NotNull WeakReference activityRef; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @NotNull SentryAndroidOptions options; private @Nullable UiElement activeUiElement = null; @@ -54,10 +54,10 @@ private enum GestureType { public SentryGestureListener( final @NotNull Activity currentActivity, - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) { this.activityRef = new WeakReference<>(currentActivity); - this.hub = hub; + this.scopes = scopes; this.options = options; } @@ -185,7 +185,7 @@ private void addBreadcrumb( hint.set(ANDROID_MOTION_EVENT, motionEvent); hint.set(ANDROID_VIEW, target.getView()); - hub.addBreadcrumb( + scopes.addBreadcrumb( Breadcrumb.userInteraction( type, target.getResourceName(), target.getClassName(), target.getTag(), additionalData), hint); @@ -202,7 +202,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull Gestur if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) { if (isNewInteraction) { - TracingUtils.startNewTrace(hub); + TracingUtils.startNewTrace(scopes); activeUiElement = target; activeEventType = eventType; } @@ -251,14 +251,13 @@ private void startTracing(final @NotNull UiElement target, final @NotNull Gestur TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); + transactionOptions.setOrigin(TRACE_ORIGIN + "." + target.getOrigin()); final ITransaction transaction = - hub.startTransaction( + scopes.startTransaction( new TransactionContext(name, TransactionNameSource.COMPONENT, op), transactionOptions); - transaction.getSpanContext().setOrigin(TRACE_ORIGIN + "." + target.getOrigin()); - - hub.configureScope( + scopes.configureScope( scope -> { applyScope(scope, transaction); }); @@ -278,7 +277,7 @@ void stopTracing(final @NotNull SpanStatus status) { activeTransaction.finish(); } } - hub.configureScope( + scopes.configureScope( scope -> { // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 0afd2bce970..76a10567d2d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -63,13 +63,8 @@ public AndroidConnectionStatusProvider( return getConnectionType(context, logger, buildInfoProvider); } - @SuppressLint("NewApi") // we have an if-check for that down below @Override public boolean addConnectionStatusObserver(final @NotNull IConnectionStatusObserver observer) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - logger.log(SentryLevel.DEBUG, "addConnectionStatusObserver requires Android 5+."); - return false; - } final ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() { @@ -103,7 +98,7 @@ public void removeConnectionStatusObserver(final @NotNull IConnectionStatusObser final @Nullable ConnectivityManager.NetworkCallback callback = registeredCallbacks.remove(observer); if (callback != null) { - unregisterNetworkCallback(context, logger, buildInfoProvider, callback); + unregisterNetworkCallback(context, logger, callback); } } @@ -253,13 +248,8 @@ public void removeConnectionStatusObserver(final @NotNull IConnectionStatusObser * @param networkCapabilities the NetworkCapabilities to check the transport type * @return the connection type wifi, ethernet, cellular or null */ - @SuppressLint("NewApi") public static @Nullable String getConnectionType( - final @NotNull NetworkCapabilities networkCapabilities, - final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - return null; - } + final @NotNull NetworkCapabilities networkCapabilities) { // TODO: change the protocol to be a list of transports as a device may have the capability of // multiple @@ -317,11 +307,8 @@ public static boolean registerNetworkCallback( public static void unregisterNetworkCallback( final @NotNull Context context, final @NotNull ILogger logger, - final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull ConnectivityManager.NetworkCallback networkCallback) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { - return; - } + final ConnectivityManager connectivityManager = getConnectivityManager(context, logger); if (connectivityManager == null) { return; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java similarity index 56% rename from sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java rename to sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java index aa54790c472..15781d711fb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java @@ -1,22 +1,28 @@ package io.sentry.android.core.internal.util; +import android.os.Handler; import android.os.Looper; +import android.os.Process; import io.sentry.protocol.SentryThread; -import io.sentry.util.thread.IMainThreadChecker; +import io.sentry.util.thread.IThreadChecker; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Class that checks if a given thread is the Android Main/UI thread */ @ApiStatus.Internal -public final class AndroidMainThreadChecker implements IMainThreadChecker { +public final class AndroidThreadChecker implements IThreadChecker { - private static final AndroidMainThreadChecker instance = new AndroidMainThreadChecker(); + private static final AndroidThreadChecker instance = new AndroidThreadChecker(); + public static volatile long mainThreadSystemId = Process.myTid(); - public static AndroidMainThreadChecker getInstance() { + public static AndroidThreadChecker getInstance() { return instance; } - private AndroidMainThreadChecker() {} + private AndroidThreadChecker() { + // The first time this class is loaded, we make sure to set the correct mainThreadId + new Handler(Looper.getMainLooper()).post(() -> mainThreadSystemId = Process.myTid()); + } @Override public boolean isMainThread(final long threadId) { @@ -38,4 +44,9 @@ public boolean isMainThread(final @NotNull SentryThread sentryThread) { final Long threadId = sentryThread.getId(); return threadId != null && isMainThread(threadId); } + + @Override + public long currentThreadSystemId() { + return Process.myTid(); + } } 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 index 8dcb994fbc9..019db99fc7d 100644 --- 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 @@ -1,5 +1,7 @@ package io.sentry.android.core.internal.util; +import io.sentry.ISentryLifecycleToken; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.FileUtils; import java.io.File; import java.io.IOException; @@ -14,6 +16,7 @@ public final class CpuInfoUtils { private static final CpuInfoUtils instance = new CpuInfoUtils(); + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public static CpuInfoUtils getInstance() { return instance; @@ -34,34 +37,36 @@ private CpuInfoUtils() {} * * @return A list with the frequency of each core of the cpu in Mhz */ - public synchronized @NotNull List readMaxFrequencies() { - if (!cpuMaxFrequenciesMhz.isEmpty()) { - return cpuMaxFrequenciesMhz; - } - File[] cpuDirs = new File(getSystemCpuPath()).listFiles(); - if (cpuDirs == null) { - return new ArrayList<>(); - } + public @NotNull List readMaxFrequencies() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + 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); + 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; + 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; + 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)); } - cpuMaxFrequenciesMhz.add((int) (khz / 1000)); + return cpuMaxFrequenciesMhz; } - return cpuMaxFrequenciesMhz; } @VisibleForTesting diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index 45e9d56877d..d6cd7bc6af9 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -14,7 +14,7 @@ import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.android.core.BuildInfoProvider; -import io.sentry.util.thread.IMainThreadChecker; +import io.sentry.util.thread.IThreadChecker; import java.io.ByteArrayOutputStream; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -31,14 +31,13 @@ public class ScreenshotUtils { final @NotNull Activity activity, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { - return takeScreenshot( - activity, AndroidMainThreadChecker.getInstance(), logger, buildInfoProvider); + return takeScreenshot(activity, AndroidThreadChecker.getInstance(), logger, buildInfoProvider); } @SuppressLint("NewApi") public static @Nullable byte[] takeScreenshot( final @NotNull Activity activity, - final @NotNull IMainThreadChecker mainThreadChecker, + final @NotNull IThreadChecker threadChecker, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { // We are keeping BuildInfoProvider param for compatibility, as it's being used by @@ -113,7 +112,7 @@ public class ScreenshotUtils { } } else { final Canvas canvas = new Canvas(bitmap); - if (mainThreadChecker.isMainThread()) { + if (threadChecker.isMainThread()) { view.draw(canvas); latch.countDown(); } else { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java index 25ff5da2bdb..ef509228dc6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java @@ -16,6 +16,7 @@ import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.SentryUUID; import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.ContextUtils; import io.sentry.util.Objects; @@ -23,7 +24,6 @@ import java.lang.reflect.Field; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; @@ -262,7 +262,7 @@ public void onActivityDestroyed(@NotNull Activity activity) {} if (!isAvailable) { return null; } - final String uid = UUID.randomUUID().toString(); + final String uid = SentryUUID.generateSentryId(); listenerMap.put(uid, listener); trackCurrentWindow(); return uid; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index ad6c4025201..5ee32b6f7bd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -10,12 +10,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.SentryDate; import io.sentry.SentryNanotimeDate; import io.sentry.TracesSamplingDecision; import io.sentry.android.core.ContextUtils; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.util.AutoClosableReentrantLock; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -43,6 +45,8 @@ public enum AppStartType { private static long CLASS_LOADED_UPTIME_MS = SystemClock.uptimeMillis(); private static volatile @Nullable AppStartMetrics instance; + public static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); private @NotNull AppStartType appStartType = AppStartType.UNKNOWN; private boolean appLaunchedInForeground = false; @@ -60,9 +64,8 @@ public enum AppStartType { private boolean shouldSendStartMeasurements = true; public static @NotNull AppStartMetrics getInstance() { - if (instance == null) { - synchronized (AppStartMetrics.class) { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { if (instance == null) { instance = new AppStartMetrics(); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityBreadcrumbsIntegrationTest.kt index 10dc60e74b9..56dabd2fbc5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityBreadcrumbsIntegrationTest.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.app.Application import android.os.Bundle import io.sentry.Breadcrumb -import io.sentry.Hub +import io.sentry.Scopes import io.sentry.SentryLevel import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -20,7 +20,7 @@ class ActivityBreadcrumbsIntegrationTest { private class Fixture { val application = mock() - val hub = mock() + val scopes = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } @@ -28,7 +28,7 @@ class ActivityBreadcrumbsIntegrationTest { fun getSut(enabled: Boolean = true): ActivityBreadcrumbsIntegration { options.isEnableActivityLifecycleBreadcrumbs = enabled - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) return ActivityBreadcrumbsIntegration( application ) @@ -40,7 +40,7 @@ class ActivityBreadcrumbsIntegrationTest { @Test fun `When ActivityBreadcrumbsIntegration is disabled, it should not register the activity callback`() { val sut = fixture.getSut(false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application, never()).registerActivityLifecycleCallbacks(any()) } @@ -48,7 +48,7 @@ class ActivityBreadcrumbsIntegrationTest { @Test fun `When ActivityBreadcrumbsIntegration is enabled, it should register the activity callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application).registerActivityLifecycleCallbacks(any()) @@ -59,12 +59,12 @@ class ActivityBreadcrumbsIntegrationTest { @Test fun `When breadcrumb is added, type and category should be set`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("ui.lifecycle", it.category) assertEquals("navigation", it.type) @@ -78,77 +78,77 @@ class ActivityBreadcrumbsIntegrationTest { @Test fun `When activity is created, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is started, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityStarted(activity) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is resumed, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityResumed(activity) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is paused, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityPaused(activity) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is stopped, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityStopped(activity) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is save instance, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivitySaveInstanceState(activity, fixture.bundle) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test fun `When activity is destroyed, it should add a breadcrumb`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityDestroyed(activity) - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt index f42d9034157..db9912c0524 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityFramesTrackerTest.kt @@ -30,6 +30,11 @@ class ActivityFramesTrackerTest { val handler = mock() val options = SentryAndroidOptions() + init { + // ActivityFramesTracker is used only if performanceV2 is disabled + options.isEnablePerformanceV2 = false + } + fun getSut(mockAggregator: Boolean = true): ActivityFramesTracker { return if (mockAggregator) { ActivityFramesTracker(loadClass, options, handler, aggregator) 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 e9021c68302..1a1d1dc7fef 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 @@ -14,10 +14,10 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.DateUtils import io.sentry.FullyDisplayedReporter -import io.sentry.Hub import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.Scopes import io.sentry.Sentry import io.sentry.SentryDate import io.sentry.SentryDateProvider @@ -72,7 +72,7 @@ class ActivityLifecycleIntegrationTest { private class Fixture { val application = mock() - val hub = mock() + val scopes = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } @@ -92,14 +92,14 @@ class ActivityLifecycleIntegrationTest { ): ActivityLifecycleIntegration { initializer?.configure(options) - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) AppStartMetrics.getInstance().isAppLaunchedInForeground = true // We let the ActivityLifecycleIntegration create the proper transaction here val optionCaptor = argumentCaptor() val contextCaptor = argumentCaptor() - whenever(hub.startTransaction(contextCaptor.capture(), optionCaptor.capture())).thenAnswer { - val t = SentryTracer(contextCaptor.lastValue, hub, optionCaptor.lastValue) + whenever(scopes.startTransaction(contextCaptor.capture(), optionCaptor.capture())).thenAnswer { + val t = SentryTracer(contextCaptor.lastValue, scopes, optionCaptor.lastValue) transaction = t return@thenAnswer t } @@ -146,7 +146,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `When ActivityLifecycleIntegration is registered, it registers activity callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application).registerActivityLifecycleCallbacks(any()) } @@ -154,7 +154,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `When ActivityLifecycleIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() @@ -164,7 +164,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `When ActivityLifecycleIntegration is closed, it should close the ActivityFramesTracker`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() @@ -174,39 +174,39 @@ class ActivityLifecycleIntegrationTest { @Test fun `When tracing is disabled, do not start tracing`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub, never()).startTransaction(any(), any()) + verify(fixture.scopes, never()).startTransaction(any(), any()) } @Test fun `When tracing is enabled but activity is running, do not start tracing again`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction(any(), any()) + verify(fixture.scopes).startTransaction(any(), any()) } @Test fun `Transaction op is ui_load and idle+deadline timeouts are set`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("ui.load", it.operation) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -214,6 +214,7 @@ class ActivityLifecycleIntegrationTest { check { transactionOptions -> assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, transactionOptions.deadlineTimeout) + assertEquals("auto.ui.activity", transactionOptions.origin) } ) } @@ -222,7 +223,7 @@ class ActivityLifecycleIntegrationTest { fun `Activity gets added to ActivityFramesTracker during transaction creation`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityStarted(activity) @@ -234,14 +235,14 @@ class ActivityLifecycleIntegrationTest { fun `Transaction name is the Activity's name`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -255,9 +256,9 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) sut.applyScope(scope, fixture.transaction) @@ -274,11 +275,11 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) - val previousTransaction = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val previousTransaction = SentryTracer(TransactionContext("name", "op"), fixture.scopes) scope.transaction = previousTransaction sut.applyScope(scope, fixture.transaction) @@ -298,14 +299,14 @@ class ActivityLifecycleIntegrationTest { it.isEnableTimeToFullDisplayTracing = true it.idleTimeout = 200 }) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) sut.ttidSpanMap.values.first().finish() sut.ttfdSpanMap.values.first().finish() // then transaction should not be immediately finished - verify(fixture.hub, never()) + verify(fixture.scopes, never()) .captureTransaction( anyOrNull(), anyOrNull(), @@ -317,7 +318,7 @@ class ActivityLifecycleIntegrationTest { Thread.sleep(400) // then the transaction should be finished - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(SpanStatus.OK, it.status) }, @@ -331,13 +332,13 @@ class ActivityLifecycleIntegrationTest { fun `When tracing auto finish is enabled, it doesn't stop the transaction on onActivityPostResumed`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction( + verify(fixture.scopes, never()).captureTransaction( any(), anyOrNull(), anyOrNull(), @@ -345,25 +346,11 @@ class ActivityLifecycleIntegrationTest { ) } - @Test - fun `When tracing auto finish is disabled, do not finish transaction`() { - val sut = fixture.getSut(initializer = { - it.tracesSampleRate = 1.0 - it.isEnableActivityLifecycleTracingAutoFinish = false - }) - sut.register(fixture.hub, fixture.options) - val activity = mock() - sut.onActivityCreated(activity, fixture.bundle) - // We don't schedule the transaction to finish - assertFalse(fixture.transaction.isFinishing()) - assertFalse(fixture.transaction.isFinished) - } - @Test fun `When tracing has status, do not overwrite it`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -373,7 +360,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPostResumed(activity) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(SpanStatus.UNKNOWN_ERROR, it.status) }, @@ -383,35 +370,49 @@ class ActivityLifecycleIntegrationTest { ) } + @Test + fun `When tracing auto finish is disabled, do not finish transaction`() { + val sut = fixture.getSut(initializer = { + it.tracesSampleRate = 1.0 + it.isEnableActivityLifecycleTracingAutoFinish = false + }) + sut.register(fixture.scopes, fixture.options) + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + // We don't schedule the transaction to finish + assertFalse(fixture.transaction.isFinishing()) + assertFalse(fixture.transaction.isFinished) + } + @Test fun `When tracing is disabled, do not finish transaction`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.scopes, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun `When Activity is destroyed but transaction is running, finish it`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.scopes).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun `When transaction is started, adds to WeakWef`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -423,7 +424,7 @@ class ActivityLifecycleIntegrationTest { fun `When Activity is destroyed removes WeakRef`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -436,7 +437,7 @@ class ActivityLifecycleIntegrationTest { fun `When Activity is destroyed, sets appStartSpan status to cancelled and finish it`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() @@ -453,7 +454,7 @@ class ActivityLifecycleIntegrationTest { fun `When Activity is destroyed, sets appStartSpan to null`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() @@ -468,7 +469,7 @@ class ActivityLifecycleIntegrationTest { fun `When Activity is destroyed, finish ttidSpan with deadline_exceeded and remove from map`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() @@ -480,6 +481,21 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTID_OP } assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) + } + + @Test + fun `When Activity is destroyed, sets ttidSpan to null`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.scopes, fixture.options) + + setAppStartTime() + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttidSpanMap[activity]) + + sut.onActivityDestroyed(activity) assertNull(sut.ttidSpanMap[activity]) } @@ -488,7 +504,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) setAppStartTime() @@ -500,6 +516,22 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTFD_OP } assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) + } + + @Test + fun `When Activity is destroyed, sets ttfdSpan to null`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.isEnableTimeToFullDisplayTracing = true + sut.register(fixture.scopes, fixture.options) + + setAppStartTime() + + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttfdSpanMap[activity]) + + sut.onActivityDestroyed(activity) assertNull(sut.ttfdSpanMap[activity]) } @@ -507,35 +539,35 @@ class ActivityLifecycleIntegrationTest { fun `When new Activity and transaction is created, finish previous ones`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(mock(), mock()) sut.onActivityCreated(mock(), fixture.bundle) - verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.scopes).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun `do not stop transaction on resumed`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) sut.onActivityResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) + verify(fixture.scopes, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) } @Test fun `start transaction on created`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(mock(), mock()) - verify(fixture.hub).startTransaction(any(), any()) + verify(fixture.scopes).startTransaction(any(), any()) } @Test @@ -544,7 +576,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true fixture.options.idleTimeout = 0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) @@ -563,7 +595,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) @@ -585,7 +617,7 @@ class ActivityLifecycleIntegrationTest { fullyDisplayedReporter = ttfdReporter } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, mock()) @@ -597,7 +629,7 @@ class ActivityLifecycleIntegrationTest { fun `App start is Cold when savedInstanceState is null`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, null) @@ -609,7 +641,7 @@ class ActivityLifecycleIntegrationTest { fun `App start is Warm when savedInstanceState is not null`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() val bundle = Bundle() @@ -622,7 +654,7 @@ class ActivityLifecycleIntegrationTest { fun `Do not overwrite App start type after set`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() val bundle = Bundle() @@ -636,7 +668,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is false, start transaction with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -648,7 +680,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) // call only once - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { assertEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) @@ -660,7 +692,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is false and app start sampling decision is set, start transaction with isAppStart true`() { AppStartMetrics.getInstance().appStartSamplingDecision = mock() val sut = fixture.getSut { it.tracesSampleRate = 1.0 } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -670,7 +702,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { assertEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) @@ -682,7 +714,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `When firstActivityCreated is false and app start sampling decision is not set, start transaction with isAppStart false`() { val sut = fixture.getSut { it.tracesSampleRate = 1.0 } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -695,7 +727,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { assertEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) @@ -709,7 +741,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is true and app start sampling decision is set, start transaction with isAppStart false`() { AppStartMetrics.getInstance().appStartSamplingDecision = mock() val sut = fixture.getSut { it.tracesSampleRate = 1.0 } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(true) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) @@ -718,14 +750,14 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction(any(), check { assertFalse(it.isAppStartTransaction) }) + verify(fixture.scopes).startTransaction(any(), check { assertFalse(it.isAppStartTransaction) }) } @Test fun `When firstActivityCreated is false and no app start time is set, default to onActivityPreCreated time`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) // usually set by SentryPerformanceProvider @@ -738,7 +770,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { assertEquals(date2.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) @@ -751,7 +783,7 @@ class ActivityLifecycleIntegrationTest { fun `When not foregroundImportance, do not create app start span`() { val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_BACKGROUND) fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually set by SentryPerformanceProvider val date = SentryNanotimeDate(Date(1), 0) @@ -762,7 +794,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) // call only once - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { assertNotEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) } ) @@ -772,7 +804,7 @@ class ActivityLifecycleIntegrationTest { fun `Create and finish app start span immediately in case SDK init is deferred`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually set by SentryPerformanceProvider val startDate = SentryNanotimeDate(Date(1), 0) @@ -780,24 +812,28 @@ class ActivityLifecycleIntegrationTest { val appStartMetrics = AppStartMetrics.getInstance() appStartMetrics.appStartType = AppStartType.WARM appStartMetrics.sdkInitTimeSpan.setStoppedAt(2) - - val endDate = appStartMetrics.sdkInitTimeSpan.projectedStopTimestamp + appStartMetrics.appStartTimeSpan.setStoppedAt(2) val activity = mock() - sut.onActivityPreCreated(activity, fixture.bundle) - sut.onActivityCreated(activity, fixture.bundle) + // An Activity already started, as SDK init is deferred + sut.onActivityPrePaused(activity) + sut.onActivityPaused(activity) + // And when we create a new Activity + sut.onActivityPreCreated(activity, null) + sut.onActivityCreated(activity, null) + sut.onActivityStopped(activity) + sut.onActivityDestroyed(activity) - val appStartSpan = fixture.transaction.children.first { it.operation.startsWith("app.start.warm") } - assertEquals(startDate.nanoTimestamp(), appStartSpan.startDate.nanoTimestamp()) - assertEquals(endDate!!.nanoTimestamp(), appStartSpan.finishDate!!.nanoTimestamp()) - assertTrue(appStartSpan.isFinished) + // No app start span is created + val appStartSpan = fixture.transaction.children.firstOrNull { it.operation.startsWith("app.start.warm") || it.operation.startsWith("app.start.cold") } + assertNull(appStartSpan) } @Test fun `When SentryPerformanceProvider is disabled, app start time span is still created`() { val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND) fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually done by SentryPerformanceProvider, if disabled it's done by // SentryAndroid.init @@ -825,7 +861,7 @@ class ActivityLifecycleIntegrationTest { fun `When app-start end time is already set, it should not be overwritten`() { val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND) fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually done by SentryPerformanceProvider val startDate = SentryNanotimeDate(Date(1), 0) @@ -849,7 +885,7 @@ class ActivityLifecycleIntegrationTest { fun `When activity lifecycle happens multiple times, app-start end time should not be overwritten`() { val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND) fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually done by SentryPerformanceProvider val startDate = SentryNanotimeDate(Date(1), 0) @@ -887,7 +923,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is false and bundle is not null, start app start warm span with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -907,7 +943,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is false and bundle is not null, start app start cold span with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -926,7 +962,7 @@ class ActivityLifecycleIntegrationTest { fun `When firstActivityCreated is false and app started more than 1 minute ago, start app with Warm start`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -942,7 +978,7 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.operation, "app.start.warm") assertEquals(span.description, "Warm Start") - assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) + assertNotEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test @@ -950,7 +986,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() AppStartMetrics.getInstance().isAppLaunchedInForeground = false fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) @@ -963,14 +999,54 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.operation, "app.start.warm") assertEquals(span.description, "Warm Start") - assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) + assertNotEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) + } + + @Test + fun `When firstActivityCreated is true and app started more than 1 minute ago, app start spans are dropped`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.scopes, fixture.options) + + val date = SentryNanotimeDate(Date(1), 0) + val duration = TimeUnit.MINUTES.toMillis(1) + 2 + val durationNanos = TimeUnit.MILLISECONDS.toNanos(duration) + val stopDate = SentryNanotimeDate(Date(duration), durationNanos) + setAppStartTime(date, stopDate) + + val activity = mock() + sut.onActivityCreated(activity, null) + + val appStartSpan = fixture.transaction.children.firstOrNull { + it.description == "Cold Start" + } + assertNull(appStartSpan) + } + + @Test + fun `When firstActivityCreated is true and app started in background, app start spans are dropped`() { + val sut = fixture.getSut() + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.scopes, fixture.options) + + val date = SentryNanotimeDate(Date(1), 0) + setAppStartTime(date) + + val activity = mock() + sut.onActivityCreated(activity, null) + + val appStartSpan = fixture.transaction.children.firstOrNull { + it.description == "Cold Start" + } + assertNull(appStartSpan) } @Test fun `When firstActivityCreated is true, start transaction but not with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(true) val date = SentryNanotimeDate(Date(1), 0) @@ -987,12 +1063,12 @@ class ActivityLifecycleIntegrationTest { 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) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) scope.transaction = fixture.transaction @@ -1010,7 +1086,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = false - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -1023,7 +1099,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -1038,7 +1114,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true fixture.options.executorService = deferredExecutorService - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) val ttfdSpan = sut.ttfdSpanMap[activity] @@ -1054,7 +1130,7 @@ class ActivityLifecycleIntegrationTest { assertEquals(SpanStatus.DEADLINE_EXCEEDED, ttfdSpan.status) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { // ttfd timed out, so its measurement should not be set val ttfdMeasurement = it.measurements[MeasurementValue.KEY_TIME_TO_FULL_DISPLAY] @@ -1071,7 +1147,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) val ttfdSpan = sut.ttfdSpanMap[activity] @@ -1094,7 +1170,7 @@ class ActivityLifecycleIntegrationTest { assertNull(autoCloseFuture) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { // ttfd was finished successfully, so its measurement should be set val ttfdMeasurement = it.measurements[MeasurementValue.KEY_TIME_TO_FULL_DISPLAY] @@ -1112,7 +1188,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() val activity2 = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -1150,7 +1226,7 @@ class ActivityLifecycleIntegrationTest { whenever(activity.findViewById(any())).thenReturn(view) // Make the integration create the spans and register to the FirstDrawDoneListener - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) sut.onActivityResumed(activity) @@ -1165,7 +1241,7 @@ class ActivityLifecycleIntegrationTest { assertTrue(ttidSpan.isFinished) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { // ttid measurement should be set val ttidMeasurement = it.measurements[MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY] @@ -1188,7 +1264,7 @@ class ActivityLifecycleIntegrationTest { whenever(activity.findViewById(any())).thenReturn(view) // Make the integration create the spans and register to the FirstDrawDoneListener - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) sut.onActivityResumed(activity) @@ -1211,7 +1287,7 @@ class ActivityLifecycleIntegrationTest { assertEquals(newEndDate, ttidSpan.finishDate) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { // ttid and ttfd measurements should be the same val ttidMeasurement = it.measurements[MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY] @@ -1233,7 +1309,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) // The ttid span should be running @@ -1255,7 +1331,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true fixture.options.executorService = deferredExecutorService - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) sut.onActivityResumed(activity) @@ -1290,7 +1366,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) val ttfdSpan = sut.ttfdSpanMap[activity] assertNotNull(ttfdSpan) @@ -1311,20 +1387,20 @@ class ActivityLifecycleIntegrationTest { fun `starts new trace if performance is disabled`() { val sut = fixture.getSut() val activity = mock() - fixture.options.enableTracing = false + fixture.options.tracesSampleRate = null val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = Scope(fixture.options) val propagationContextAtStart = scope.propagationContext - whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { + whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) // once for the screen, and once for the tracing propagation context - verify(fixture.hub, times(2)).configureScope(any()) + verify(fixture.scopes, times(2)).configureScope(any()) assertNotSame(propagationContextAtStart, scope.propagationContext) } @@ -1332,19 +1408,19 @@ class ActivityLifecycleIntegrationTest { fun `sets the activity as the current screen`() { val sut = fixture.getSut() val activity = mock() - fixture.options.enableTracing = false + fixture.options.tracesSampleRate = null val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = mock() - whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { + whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) // once for the screen, and once for the tracing propagation context - verify(fixture.hub, times(2)).configureScope(any()) + verify(fixture.scopes, times(2)).configureScope(any()) verify(scope).setScreen(any()) } @@ -1352,37 +1428,37 @@ class ActivityLifecycleIntegrationTest { fun `does not start another new trace if one has already been started but does after activity was destroyed`() { val sut = fixture.getSut() val activity = mock() - fixture.options.enableTracing = false + fixture.options.tracesSampleRate = null val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = Scope(fixture.options) val propagationContextAtStart = scope.propagationContext - whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { + whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, fixture.bundle) // once for the screen, and once for the tracing propagation context - verify(fixture.hub, times(2)).configureScope(any()) + verify(fixture.scopes, times(2)).configureScope(any()) val propagationContextAfterNewTrace = scope.propagationContext assertNotSame(propagationContextAtStart, propagationContextAfterNewTrace) - clearInvocations(fixture.hub) + clearInvocations(fixture.scopes) sut.onActivityCreated(activity, fixture.bundle) // once for the screen, but not for the tracing propagation context - verify(fixture.hub).configureScope(any()) + verify(fixture.scopes).configureScope(any()) assertSame(propagationContextAfterNewTrace, scope.propagationContext) sut.onActivityDestroyed(activity) - clearInvocations(fixture.hub) + clearInvocations(fixture.scopes) sut.onActivityCreated(activity, fixture.bundle) // once for the screen, and once for the tracing propagation context - verify(fixture.hub, times(2)).configureScope(any()) + verify(fixture.scopes, times(2)).configureScope(any()) assertNotSame(propagationContextAfterNewTrace, scope.propagationContext) } @@ -1390,7 +1466,7 @@ class ActivityLifecycleIntegrationTest { fun `when transaction is finished, sets frame metrics`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val activity = mock() sut.onActivityCreated(activity, fixture.bundle) @@ -1406,7 +1482,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.tracesSampleRate = 1.0 fixture.options.dateProvider = SentryDateProvider { now } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // usually done by SentryPerformanceProvider val startDate = SentryNanotimeDate(Date(5678), 910) @@ -1431,7 +1507,7 @@ class ActivityLifecycleIntegrationTest { fun `On activity preCreated onCreate span is started`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) @@ -1457,7 +1533,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.dateProvider = SentryDateProvider { startDate } setAppStartTime(appStartDate) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityPreCreated(activity, null) @@ -1492,7 +1568,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() setAppStartTime() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityPreCreated(activity, null) @@ -1516,7 +1592,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.dateProvider = SentryDateProvider { startDate } setAppStartTime(appStartDate) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityCreated(activity, null) @@ -1545,7 +1621,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() setAppStartTime() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityCreated(activity, null) @@ -1565,7 +1641,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() fixture.options.dateProvider = SentryDateProvider { startDate } setAppStartTime(appStartDate) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.setFirstActivityCreated(true) sut.onActivityPreCreated(activity, null) @@ -1587,7 +1663,7 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(appStartDate) // Let's pretend app start started and finished appStartMetrics.appStartTimeSpan.stop() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertEquals(0, sut.getProperty("lastPausedUptimeMillis")) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt index 359fee49cc0..d10cdea35e1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt @@ -4,9 +4,6 @@ import android.content.Context import android.content.pm.PackageManager.PERMISSION_DENIED import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback -import android.net.ConnectivityManager.TYPE_ETHERNET -import android.net.ConnectivityManager.TYPE_MOBILE -import android.net.ConnectivityManager.TYPE_WIFI import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkCapabilities.TRANSPORT_CELLULAR @@ -112,14 +109,6 @@ class AndroidConnectionStatusProviderTest { assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) } - @Test - fun `When sdkInfoVersion is not min Marshmallow, return null for getConnectionType`() { - val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - - assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) - } - @Test fun `When there's no permission, return null for getConnectionType`() { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) @@ -146,14 +135,6 @@ class AndroidConnectionStatusProviderTest { assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } - @Test - fun `When network is TYPE_WIFI, return wifi`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - whenever(networkInfo.type).thenReturn(TYPE_WIFI) - - assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) - } - @Test fun `When network capabilities has TRANSPORT_ETHERNET, return ethernet`() { whenever(networkCapabilities.hasTransport(eq(TRANSPORT_ETHERNET))).thenReturn(true) @@ -164,17 +145,6 @@ class AndroidConnectionStatusProviderTest { ) } - @Test - fun `When network is TYPE_ETHERNET, return ethernet`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - whenever(networkInfo.type).thenReturn(TYPE_ETHERNET) - - assertEquals( - "ethernet", - AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) - ) - } - @Test fun `When network capabilities has TRANSPORT_CELLULAR, return cellular`() { whenever(networkCapabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true) @@ -185,17 +155,6 @@ class AndroidConnectionStatusProviderTest { ) } - @Test - fun `When network is TYPE_MOBILE, return cellular`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - whenever(networkInfo.type).thenReturn(TYPE_MOBILE) - - assertEquals( - "cellular", - AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) - ) - } - @Test fun `When there's no permission, do not register any NetworkCallback`() { val buildInfo = mock() @@ -231,20 +190,10 @@ class AndroidConnectionStatusProviderTest { verify(connectivityManager).registerDefaultNetworkCallback(any()) } - @Test - fun `When sdkInfoVersion is not min Lollipop, do not unregister any NetworkCallback`() { - val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) - - verify(connectivityManager, never()).unregisterNetworkCallback(any()) - } - @Test fun `unregisterNetworkCallback calls connectivityManager unregisterDefaultNetworkCallback`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), mock()) verify(connectivityManager).unregisterNetworkCallback(any()) } @@ -257,15 +206,6 @@ class AndroidConnectionStatusProviderTest { assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } - @Test - fun `When connectivityManager getActiveNetworkInfo throws an exception, getConnectionType returns null`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - whenever(connectivityManager.activeNetworkInfo).thenThrow(SecurityException("Android OS Bug")) - - assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) - assertEquals(IConnectionStatusProvider.ConnectionStatus.UNKNOWN, connectionStatusProvider.connectionStatus) - } - @Test fun `When connectivityManager registerDefaultCallback throws an exception, false is returned`() { whenever(connectivityManager.registerDefaultNetworkCallback(any())).thenThrow( @@ -289,7 +229,7 @@ class AndroidConnectionStatusProviderTest { var failed = false try { - AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), mock()) } catch (t: Throwable) { failed = true } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt index 45010255e8d..7f54b2c3539 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt @@ -1,11 +1,9 @@ package io.sentry.android.core -import android.os.Build import io.sentry.ILogger import io.sentry.PerformanceCollectionData import io.sentry.test.getCtor import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals @@ -15,16 +13,11 @@ import kotlin.test.assertNull class AndroidCpuCollectorTest { private val className = "io.sentry.android.core.AndroidCpuCollector" - private val ctorTypes = arrayOf(ILogger::class.java, BuildInfoProvider::class.java) + private val ctorTypes = arrayOf>(ILogger::class.java) private val fixture = Fixture() private class Fixture { - private val mockBuildInfoProvider = mock() - init { - whenever(mockBuildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) - } - fun getSut(buildInfoProvider: BuildInfoProvider = mockBuildInfoProvider) = - AndroidCpuCollector(mock(), buildInfoProvider) + fun getSut() = AndroidCpuCollector(mock()) } @Test @@ -32,10 +25,7 @@ class AndroidCpuCollectorTest { val ctor = className.getCtor(ctorTypes) assertFailsWith { - ctor.newInstance(arrayOf(null, mock())) - } - assertFailsWith { - ctor.newInstance(arrayOf(mock(), null)) + ctor.newInstance(arrayOf(mock())) } } @@ -57,15 +47,4 @@ class AndroidCpuCollectorTest { assertNotEquals(0.0, cpuData.cpuUsagePercentage) assertNotEquals(0, cpuData.timestampMillis) } - - @Test - fun `collector works only on api 21+`() { - val data = PerformanceCollectionData() - val mockBuildInfoProvider = mock() - whenever(mockBuildInfoProvider.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - val collector = fixture.getSut(mockBuildInfoProvider) - collector.setup() - collector.collect(data) - assertNull(data.cpuData) - } } 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 ed2fa3338a5..95ca59a06e2 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 @@ -13,7 +13,7 @@ import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader -import io.sentry.android.core.internal.util.AndroidMainThreadChecker +import io.sentry.android.core.internal.util.AndroidThreadChecker import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.replay.ReplayIntegration import io.sentry.android.timber.SentryTimberIntegration @@ -98,7 +98,6 @@ class AndroidOptionsInitializerTest { } fun initSutWithClassLoader( - minApi: Int = Build.VERSION_CODES.KITKAT, classesToLoad: List = emptyList(), isFragmentAvailable: Boolean = false, isTimberAvailable: Boolean = false, @@ -111,7 +110,7 @@ class AndroidOptionsInitializerTest { } ) sentryOptions.isDebug = true - val buildInfo = createBuildInfo(minApi) + val buildInfo = createBuildInfo() val loadClass = createClassMock(classesToLoad) val activityFramesTracker = ActivityFramesTracker(loadClass, sentryOptions) @@ -142,9 +141,9 @@ class AndroidOptionsInitializerTest { ) } - private fun createBuildInfo(minApi: Int): BuildInfoProvider { + private fun createBuildInfo(): BuildInfoProvider { val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) return buildInfo } @@ -524,7 +523,7 @@ class AndroidOptionsInitializerTest { } @Test - fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be available`() { + fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be unavailable`() { fixture.initSut( hasAppContext = true, useRealContext = true, @@ -533,6 +532,25 @@ class AndroidOptionsInitializerTest { } ) + val activityLifeCycleIntegration = fixture.sentryOptions.integrations + .first { it is ActivityLifecycleIntegration } + + assertFalse( + (activityLifeCycleIntegration as ActivityLifecycleIntegration).activityFramesTracker.isFrameMetricsAggregatorAvailable + ) + } + + @Test + fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be available if perfv2 is false`() { + fixture.initSut( + hasAppContext = true, + useRealContext = true, + configureOptions = { + isEnablePerformanceV2 = false + isEnableFramesTracking = true + } + ) + val activityLifeCycleIntegration = fixture.sentryOptions.integrations .first { it is ActivityLifecycleIntegration } @@ -556,12 +574,32 @@ class AndroidOptionsInitializerTest { } @Test - fun `When Frames Tracking is initially disabled, but enabled via configureOptions it should be available`() { + fun `When Frames Tracking is initially disabled, but enabled via configureOptions it should be unavailable`() { + fixture.sentryOptions.isEnableFramesTracking = false + fixture.initSut( + hasAppContext = true, + useRealContext = true, + configureOptions = { + isEnableFramesTracking = true + } + ) + + val activityLifeCycleIntegration = fixture.sentryOptions.integrations + .first { it is ActivityLifecycleIntegration } + + assertFalse( + (activityLifeCycleIntegration as ActivityLifecycleIntegration).activityFramesTracker.isFrameMetricsAggregatorAvailable + ) + } + + @Test + fun `When Frames Tracking is initially disabled, but enabled via configureOptions it should be available if perfv2 is false`() { fixture.sentryOptions.isEnableFramesTracking = false fixture.initSut( hasAppContext = true, useRealContext = true, configureOptions = { + isEnablePerformanceV2 = false isEnableFramesTracking = true } ) @@ -582,10 +620,10 @@ class AndroidOptionsInitializerTest { } @Test - fun `AndroidMainThreadChecker is set to options`() { + fun `AndroidThreadChecker is set to options`() { fixture.initSut() - assertTrue { fixture.sentryOptions.mainThreadChecker is AndroidMainThreadChecker } + assertTrue { fixture.sentryOptions.threadChecker is AndroidThreadChecker } } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index c5bb334bb3b..670729c4d76 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -1,7 +1,6 @@ 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 io.sentry.CpuCollectionData @@ -42,14 +41,11 @@ class AndroidProfilerTest { private lateinit var context: Context private val className = "io.sentry.android.core.AndroidProfiler" - private val ctorTypes = arrayOf(String::class.java, Int::class.java, SentryFrameMetricsCollector::class.java, ISentryExecutorService::class.java, ILogger::class.java, BuildInfoProvider::class.java) + private val ctorTypes = arrayOf(String::class.java, Int::class.java, SentryFrameMetricsCollector::class.java, ISentryExecutorService::class.java, ILogger::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() var lastScheduledRunnable: Runnable? = null val mockExecutorService = object : ISentryExecutorService { @@ -86,14 +82,13 @@ class AndroidProfilerTest { val frameMetricsCollector: SentryFrameMetricsCollector = mock() - fun getSut(interval: Int = 1, buildInfoProvider: BuildInfoProvider = buildInfo): AndroidProfiler { + fun getSut(interval: Int = 1): AndroidProfiler { return AndroidProfiler( options.profilingTracesDirPath!!, interval, frameMetricsCollector, options.executorService, - options.logger, - buildInfoProvider + options.logger ) } } @@ -144,34 +139,19 @@ class AndroidProfilerTest { val ctor = className.getCtor(ctorTypes) assertFailsWith { - ctor.newInstance(arrayOf(null, 0, mock(), mock(), mock(), mock())) - } - assertFailsWith { - ctor.newInstance(arrayOf("mock", 0, null, mock(), mock(), mock())) + ctor.newInstance(arrayOf(null, 0, mock(), mock(), mock())) } assertFailsWith { - ctor.newInstance(arrayOf("mock", 0, mock(), null, mock(), mock())) + ctor.newInstance(arrayOf("mock", 0, null, mock(), mock())) } assertFailsWith { - ctor.newInstance(arrayOf("mock", 0, mock(), mock(), null, mock())) + ctor.newInstance(arrayOf("mock", 0, mock(), null, mock())) } assertFailsWith { - ctor.newInstance(arrayOf("mock", 0, mock(), mock(), mock(), null)) + ctor.newInstance(arrayOf("mock", 0, mock(), mock(), null)) } } - @Test - fun `profiler works only on api 21+`() { - val buildInfo = mock { - whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - } - val profiler = fixture.getSut(1, buildInfo) - val startData = profiler.start() - val endData = profiler.endAndCollect(false, null) - assertNull(startData) - assertNull(endData) - } - @Test fun `profiler returns start and end timestamps`() { val profiler = fixture.getSut() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index df64e20b16f..b92db7cfc62 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -5,8 +5,8 @@ import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.CpuCollectionData -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData @@ -89,7 +89,7 @@ class AndroidTransactionProfilerTest { executorService = mockExecutorService } - val hub: IHub = mock() + val scopes: IScopes = mock() val frameMetricsCollector: SentryFrameMetricsCollector = mock() lateinit var transaction1: SentryTracer @@ -97,10 +97,10 @@ class AndroidTransactionProfilerTest { lateinit var transaction3: SentryTracer fun getSut(context: Context, buildInfoProvider: BuildInfoProvider = buildInfo): AndroidTransactionProfiler { - whenever(hub.options).thenReturn(options) - transaction1 = SentryTracer(TransactionContext("", ""), hub) - transaction2 = SentryTracer(TransactionContext("", ""), hub) - transaction3 = SentryTracer(TransactionContext("", ""), hub) + whenever(scopes.options).thenReturn(options) + transaction1 = SentryTracer(TransactionContext("", ""), scopes) + transaction2 = SentryTracer(TransactionContext("", ""), scopes) + transaction3 = SentryTracer(TransactionContext("", ""), scopes) return AndroidTransactionProfiler(context, options, buildInfoProvider, frameMetricsCollector) } } @@ -336,16 +336,6 @@ class AndroidTransactionProfilerTest { assertEquals(0, profiler.transactionsCounter) } - @Test - fun `profiler ignores profilingTracesIntervalMillis`() { - fixture.options.apply { - profilingTracesIntervalMillis = 0 - } - val profiler = fixture.getSut(context) - profiler.start() - assertEquals(1, profiler.transactionsCounter) - } - @Test fun `profiler never use background threads`() { val profiler = fixture.getSut(context) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt index cceabc9774f..1a74a47ae1e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt @@ -2,7 +2,7 @@ package io.sentry.android.core import android.content.Context import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.android.core.AnrIntegration.AnrHint import io.sentry.exception.ExceptionMechanismException @@ -24,7 +24,7 @@ class AnrIntegrationTest { private class Fixture { val context = mock() - val hub = mock() + val scopes = mock() var options: SentryAndroidOptions = SentryAndroidOptions().apply { setLogger(mock()) } @@ -49,7 +49,7 @@ class AnrIntegrationTest { fixture.options.executorService = ImmediateExecutorService() val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.anrWatchDog) assertTrue((sut.anrWatchDog as ANRWatchDog).isAlive) @@ -60,7 +60,7 @@ class AnrIntegrationTest { fixture.options.executorService = mock() val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNull(sut.anrWatchDog) } @@ -70,7 +70,7 @@ class AnrIntegrationTest { val sut = fixture.getSut() fixture.options.isAnrEnabled = false - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNull(sut.anrWatchDog) } @@ -79,9 +79,9 @@ class AnrIntegrationTest { fun `When ANR watch dog is triggered, it should capture an error event with AnrHint`() { val sut = fixture.getSut() - sut.reportANR(fixture.hub, fixture.options, getApplicationNotResponding()) + sut.reportANR(fixture.scopes, fixture.options, getApplicationNotResponding()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.ERROR, it.level) }, @@ -97,7 +97,7 @@ class AnrIntegrationTest { val sut = fixture.getSut() fixture.options.executorService = ImmediateExecutorService() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.anrWatchDog) @@ -107,11 +107,11 @@ class AnrIntegrationTest { } @Test - fun `when hub is closed right after start, integration is not registered`() { + fun `when scopes is closed right after start, integration is not registered`() { val deferredExecutorService = DeferredExecutorService() val sut = fixture.getSut() fixture.options.executorService = deferredExecutorService - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNull(sut.anrWatchDog) sut.close() deferredExecutorService.runAll() @@ -122,9 +122,9 @@ class AnrIntegrationTest { fun `When ANR watch dog is triggered, constructs exception with proper mechanism and snapshot flag`() { val sut = fixture.getSut() - sut.reportANR(fixture.hub, fixture.options, getApplicationNotResponding()) + sut.reportANR(fixture.scopes, fixture.options, getApplicationNotResponding()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val ex = it.throwableMechanism as ExceptionMechanismException assertTrue(ex.isSnapshot) @@ -139,9 +139,9 @@ class AnrIntegrationTest { val sut = fixture.getSut() AppState.getInstance().setInBackground(true) - sut.reportANR(fixture.hub, fixture.options, getApplicationNotResponding()) + sut.reportANR(fixture.scopes, fixture.options, getApplicationNotResponding()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val message = it.throwable?.message assertTrue(message?.startsWith("Background") == true) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index 80ae9467114..d930333f4c2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -115,7 +115,7 @@ class AnrV2EventProcessorTest { persistScope( CONTEXTS_FILENAME, Contexts().apply { - trace = SpanContext("test") + setTrace(SpanContext("test")) setResponse(Response().apply { bodySize = 1024 }) setBrowser(Browser().apply { name = "Google Chrome" }) } @@ -179,7 +179,7 @@ class AnrV2EventProcessorTest { assertNull(processed.platform) assertNull(processed.exceptions) - assertEquals(emptyMap(), processed.contexts) + assertTrue(processed.contexts.isEmpty) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index a658a245051..68339a4b797 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -6,8 +6,8 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Hint -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.SentryEnvelope import io.sentry.SentryLevel import io.sentry.android.core.AnrV2Integration.AnrV2Hint @@ -59,7 +59,7 @@ class AnrV2IntegrationTest { lateinit var lastReportedAnrFile: File val options = SentryAndroidOptions() - val hub = mock() + val scopes = mock() val logger = mock() fun getSut( @@ -93,7 +93,7 @@ class AnrV2IntegrationTest { lastReportedAnrFile = File(cacheDir, AndroidEnvelopeCache.LAST_ANR_REPORT) lastReportedAnrFile.writeText(lastReportedAnrTimestamp.toString()) } - whenever(hub.captureEvent(any(), anyOrNull())).thenReturn(lastEventId) + whenever(scopes.captureEvent(any(), anyOrNull())).thenReturn(lastEventId) return AnrV2Integration(context) } @@ -200,7 +200,7 @@ class AnrV2IntegrationTest { fun `when cacheDir is not set, does not process historical exits`() { val integration = fixture.getSut(null, useImmediateExecutorService = false) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.options.executorService, never()).submit(any()) } @@ -210,7 +210,7 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, isAnrEnabled = false, useImmediateExecutorService = false) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.options.executorService, never()).submit(any()) } @@ -219,9 +219,9 @@ class AnrV2IntegrationTest { fun `when historical exit list is empty, does not process historical exits`() { val integration = fixture.getSut(tmpDir) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test @@ -229,9 +229,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir) fixture.addAppExitInfo(reason = null) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test @@ -242,9 +242,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir) fixture.addAppExitInfo(timestamp = oldTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test @@ -252,9 +252,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = oldTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test @@ -262,9 +262,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = null) fixture.addAppExitInfo(timestamp = oldTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent(any(), anyOrNull()) + verify(fixture.scopes).captureEvent(any(), anyOrNull()) } @Test @@ -272,9 +272,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(newTimestamp, it.timestamp.time) assertEquals(SentryLevel.FATAL, it.level) @@ -321,9 +321,9 @@ class AnrV2IntegrationTest { importance = ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND ) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -341,7 +341,7 @@ class AnrV2IntegrationTest { ) fixture.addAppExitInfo(timestamp = newTimestamp) - whenever(fixture.hub.captureEvent(any(), any())).thenAnswer { invocation -> + whenever(fixture.scopes.captureEvent(any(), any())).thenAnswer { invocation -> val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1)) as DiskFlushNotification thread { @@ -351,9 +351,9 @@ class AnrV2IntegrationTest { SentryId() } - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent(any(), anyOrNull()) + verify(fixture.scopes).captureEvent(any(), anyOrNull()) // shouldn't fall into timed out state, because we marked event as flushed on another thread verify(fixture.logger, never()).log( any(), @@ -371,9 +371,9 @@ class AnrV2IntegrationTest { ) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent(any(), anyOrNull()) + verify(fixture.scopes).captureEvent(any(), anyOrNull()) // we do not call markFlushed, hence it should time out waiting for flush, but because // we drop the event, it should not even come to this if-check verify(fixture.logger, never()).log( @@ -390,9 +390,9 @@ class AnrV2IntegrationTest { fixture.addAppExitInfo(timestamp = newTimestamp - 1 * 60 * 1000) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, times(2)).captureEvent( + verify(fixture.scopes, times(2)).captureEvent( any(), argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -412,10 +412,10 @@ class AnrV2IntegrationTest { fixture.addAppExitInfo(timestamp = newTimestamp - 1 * 60 * 1000) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) // only the latest anr is reported which should be enrichable - verify(fixture.hub, atMost(1)).captureEvent( + verify(fixture.scopes, atMost(1)).captureEvent( any(), argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -432,20 +432,20 @@ class AnrV2IntegrationTest { fixture.addAppExitInfo(timestamp = newTimestamp - TimeUnit.DAYS.toMillis(1)) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) // the order is reverse here, so the oldest ANR will be reported first to keep track of // last reported ANR in a marker file - inOrder(fixture.hub) { - verify(fixture.hub).captureEvent( + inOrder(fixture.scopes) { + verify(fixture.scopes).captureEvent( argThat { timestamp.time == newTimestamp - TimeUnit.DAYS.toMillis(2) }, anyOrNull() ) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( argThat { timestamp.time == newTimestamp - TimeUnit.DAYS.toMillis(1) }, anyOrNull() ) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( argThat { timestamp.time == newTimestamp }, anyOrNull() ) @@ -457,9 +457,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -473,9 +473,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), argThat { val hint = HintUtils.getSentrySdkHint(this) @@ -502,7 +502,7 @@ class AnrV2IntegrationTest { ) } - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) // we store envelope with StartSessionHint on different thread after some delay, which // triggers the previous session flush, so no timeout @@ -523,14 +523,14 @@ class AnrV2IntegrationTest { ) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.logger, never()).log( any(), argThat { startsWith("Timed out waiting to flush previous session to its own file.") }, any() ) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test @@ -542,7 +542,7 @@ class AnrV2IntegrationTest { ) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.logger).log( any(), @@ -562,9 +562,9 @@ class AnrV2IntegrationTest { ) fixture.addAppExitInfo(timestamp = newTimestamp) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), check { assertNotNull(it.threadDump) @@ -577,9 +577,9 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = newTimestamp, addTrace = false) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test @@ -587,8 +587,8 @@ class AnrV2IntegrationTest { val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) fixture.addAppExitInfo(timestamp = newTimestamp, addBadTrace = true) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) - verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } } 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 8f45c238029..9ae0c1c1c0f 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,7 +5,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.test.ImmediateExecutorService import org.junit.runner.RunWith @@ -40,8 +40,8 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) verify(fixture.context).registerComponentCallbacks(any()) } @@ -51,10 +51,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) whenever(fixture.context.registerComponentCallbacks(any())).thenThrow(NullPointerException()) - sut.register(hub, options) + sut.register(scopes, options) assertFalse(options.isEnableAppComponentBreadcrumbs) } @@ -65,8 +65,8 @@ class AppComponentsBreadcrumbsIntegrationTest { isEnableAppComponentBreadcrumbs = false executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) verify(fixture.context, never()).registerComponentCallbacks(any()) } @@ -76,8 +76,8 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) sut.close() verify(fixture.context).unregisterComponentCallbacks(any()) } @@ -88,10 +88,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() + val scopes = mock() whenever(fixture.context.registerComponentCallbacks(any())).thenThrow(NullPointerException()) whenever(fixture.context.unregisterComponentCallbacks(any())).thenThrow(NullPointerException()) - sut.register(hub, options) + sut.register(scopes, options) sut.close() } @@ -101,10 +101,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) sut.onLowMemory() - verify(hub).addBreadcrumb( + verify(scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -119,10 +119,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) sut.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) - verify(hub).addBreadcrumb( + verify(scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -137,10 +137,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) sut.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) - verify(hub, never()).addBreadcrumb(any()) + verify(scopes, never()).addBreadcrumb(any()) } @Test @@ -149,10 +149,10 @@ class AppComponentsBreadcrumbsIntegrationTest { val options = SentryAndroidOptions().apply { executorService = ImmediateExecutorService() } - val hub = mock() - sut.register(hub, options) + val scopes = mock() + sut.register(scopes, options) sut.onConfigurationChanged(mock()) - verify(hub).addBreadcrumb( + verify(scopes).addBreadcrumb( check { assertEquals("device.orientation", it.category) assertEquals("navigation", it.type) 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 ed8d53227ce..733aefa8d63 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 @@ -2,7 +2,7 @@ package io.sentry.android.core import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.IScopes import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -17,7 +17,7 @@ import kotlin.test.assertNull class AppLifecycleIntegrationTest { private class Fixture { - val hub = mock() + val scopes = mock() lateinit var handler: MainLooperHandler val options = SentryAndroidOptions() @@ -33,7 +33,7 @@ class AppLifecycleIntegrationTest { fun `When AppLifecycleIntegration is added, lifecycle watcher should be started`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.watcher) } @@ -46,7 +46,7 @@ class AppLifecycleIntegrationTest { isEnableAutoSessionTracking = false } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNull(sut.watcher) } @@ -55,7 +55,7 @@ class AppLifecycleIntegrationTest { fun `When AppLifecycleIntegration is closed, lifecycle watcher should be closed`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.watcher) @@ -70,7 +70,7 @@ class AppLifecycleIntegrationTest { val latch = CountDownLatch(1) Thread { - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) latch.countDown() }.start() @@ -84,7 +84,7 @@ class AppLifecycleIntegrationTest { val sut = fixture.getSut() val latch = CountDownLatch(1) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.watcher) @@ -103,7 +103,7 @@ class AppLifecycleIntegrationTest { val sut = fixture.getSut(mockHandler = false) val latch = CountDownLatch(1) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(sut.watcher) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index 588a32a6569..04702dc0ee2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -146,7 +146,7 @@ class ContextUtilsTest { @Test fun `when supported abis is specified, getArchitectures returns correct values`() { - val architectures = ContextUtils.getArchitectures(BuildInfoProvider(logger)) + val architectures = ContextUtils.getArchitectures() assertEquals("armeabi-v7a", architectures[0]) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt index 63306231214..ecdbff5104f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt @@ -3,7 +3,7 @@ package io.sentry.android.core import android.app.Activity import android.app.Application import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.IScopes import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -19,7 +19,7 @@ class CurrentActivityIntegrationTest { private class Fixture { val application = mock() val activity = mock() - val hub = mock() + val scopes = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" @@ -27,7 +27,7 @@ class CurrentActivityIntegrationTest { fun getSut(): CurrentActivityIntegration { val integration = CurrentActivityIntegration(application) - integration.register(hub, options) + integration.register(scopes, options) return integration } } 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 80954f67a5f..9aac83ebada 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 @@ -7,7 +7,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.DiagnosticLogger import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.SentryTracer @@ -62,13 +62,13 @@ class DefaultAndroidEventProcessorTest { sdkVersion = SdkVersion("test", "1.2.3") } - val hub: IHub = mock() + val scopes: IScopes = mock() lateinit var sentryTracer: SentryTracer fun getSut(context: Context): DefaultAndroidEventProcessor { - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("", ""), hub) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("", ""), scopes) return DefaultAndroidEventProcessor(context, buildInfo, options) } } @@ -493,12 +493,11 @@ class DefaultAndroidEventProcessorTest { } @Test - fun `Event sets language and locale`() { + fun `Event sets locale`() { val sut = fixture.getSut(context) 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/EnvelopeFileObserverIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverIntegrationTest.kt index 699fa2d2f27..9aa7382dcc4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/EnvelopeFileObserverIntegrationTest.kt @@ -1,13 +1,13 @@ package io.sentry.android.core import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.Hub -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.test.DeferredExecutorService import io.sentry.test.ImmediateExecutorService +import io.sentry.test.createTestScopes import org.junit.runner.RunWith import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -24,7 +24,7 @@ import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) class EnvelopeFileObserverIntegrationTest { inner class Fixture { - val hub: IHub = mock() + val scopes: IScopes = mock() private lateinit var options: SentryAndroidOptions val logger = mock() @@ -33,7 +33,7 @@ class EnvelopeFileObserverIntegrationTest { options.setLogger(logger) options.isDebug = true optionConfiguration(options) - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) return object : EnvelopeFileObserverIntegration() { override fun getPath(options: SentryOptions): String? = file.absolutePath @@ -52,7 +52,12 @@ class EnvelopeFileObserverIntegrationTest { @AfterTest fun shutdown() { - Files.delete(file.toPath()) + delete(file) + } + + private fun delete(f: File) { + f.listFiles()?.forEach { delete(it) } + Files.delete(f.toPath()) } @Test @@ -65,27 +70,25 @@ class EnvelopeFileObserverIntegrationTest { } @Test - fun `when hub is closed, integrations should be closed`() { + fun `when scopes is closed, integrations should be closed`() { val integrationMock = mock() val options = SentryOptions() options.dsn = "https://key@sentry.io/proj" options.cacheDirPath = file.absolutePath options.addIntegration(integrationMock) options.setSerializer(mock()) -// val expected = HubAdapter.getInstance() - val hub = Hub(options) -// verify(integrationMock).register(expected, options) - hub.close() + val scopes = createTestScopes(options) + scopes.close() verify(integrationMock).close() } @Test - fun `when hub is closed right after start, integration is not registered`() { + fun `when scopes is closed right after start, integration is not registered`() { val deferredExecutorService = DeferredExecutorService() val integration = fixture.getSut { it.executorService = deferredExecutorService } - integration.register(fixture.hub, fixture.hub.options) + integration.register(fixture.scopes, fixture.scopes.options) integration.close() deferredExecutorService.runAll() verify(fixture.logger, never()).log(eq(SentryLevel.DEBUG), eq("EnvelopeFileObserverIntegration installed.")) @@ -96,7 +99,7 @@ class EnvelopeFileObserverIntegrationTest { val integration = fixture.getSut { it.executorService = mock() } - integration.register(fixture.hub, fixture.hub.options) + integration.register(fixture.scopes, fixture.scopes.options) verify(fixture.logger).log( eq(SentryLevel.DEBUG), eq("Registering EnvelopeFileObserverIntegration for path: %s"), @@ -110,7 +113,7 @@ class EnvelopeFileObserverIntegrationTest { val integration = fixture.getSut { it.executorService = ImmediateExecutorService() } - integration.register(fixture.hub, fixture.hub.options) + integration.register(fixture.scopes, fixture.scopes.options) verify(fixture.logger).log( eq(SentryLevel.DEBUG), eq("Registering EnvelopeFileObserverIntegration for path: %s"), diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 3aa8cb575ef..01b9845a9fe 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -7,9 +7,9 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.Hub import io.sentry.IScope import io.sentry.Scope +import io.sentry.ScopeType import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader @@ -27,6 +27,7 @@ import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism import io.sentry.protocol.SentryId import io.sentry.protocol.User +import io.sentry.test.createTestScopes import io.sentry.transport.ITransport import io.sentry.transport.RateLimiter import org.junit.runner.RunWith @@ -54,7 +55,7 @@ class InternalSentrySdkTest { lateinit var options: SentryOptions fun init(context: Context) { - SentryAndroid.init(context) { options -> + initForTest(context) { options -> this@Fixture.options = options options.dsn = "https://key@host/proj" options.setTransportFactory { _, _ -> @@ -87,7 +88,7 @@ class InternalSentrySdkTest { fun captureEnvelopeWithEvent(event: SentryEvent = SentryEvent(), maybeStartNewSession: Boolean = false) { // create an envelope with session data - val options = Sentry.getCurrentHub().options + val options = Sentry.getCurrentScopes().options val eventId = SentryId() val header = SentryEnvelopeHeader(eventId) val eventItem = SentryEnvelopeItem.fromEvent(options.serializer, event) @@ -202,40 +203,34 @@ class InternalSentrySdkTest { @BeforeTest fun `set up`() { + Sentry.close() context = ApplicationProvider.getApplicationContext() DeviceInfoUtil.resetInstance() } @Test - fun `current scope returns null when hub is no-op`() { - Sentry.getCurrentHub().close() + fun `current scope returns null when scopes is no-op`() { + Sentry.setCurrentScopes(createTestScopes(enabled = false)) val scope = InternalSentrySdk.getCurrentScope() assertNull(scope) } @Test - fun `current scope returns obj when hub is active`() { - Sentry.setCurrentHub( - Hub( - SentryOptions().apply { - dsn = "https://key@uri/1234567" - } - ) - ) + fun `current scope returns obj when scopes is active`() { + val fixture = Fixture() + fixture.init(context) val scope = InternalSentrySdk.getCurrentScope() assertNotNull(scope) } @Test fun `current scope returns a copy of the scope`() { - Sentry.setCurrentHub( - Hub( - SentryOptions().apply { - dsn = "https://key@uri/1234567" - } - ) - ) + val fixture = Fixture() + fixture.init(context) Sentry.addBreadcrumb("test") + Sentry.configureScope(ScopeType.CURRENT) { scope -> scope.addBreadcrumb(Breadcrumb("currentBreadcrumb")) } + Sentry.configureScope(ScopeType.ISOLATION) { scope -> scope.addBreadcrumb(Breadcrumb("isolationBreadcrumb")) } + Sentry.configureScope(ScopeType.GLOBAL) { scope -> scope.addBreadcrumb(Breadcrumb("globalBreadcrumb")) } // when the clone is modified val clonedScope = InternalSentrySdk.getCurrentScope()!! @@ -243,7 +238,7 @@ class InternalSentrySdkTest { // then modifications should not be reflected Sentry.configureScope { scope -> - assertEquals(1, scope.breadcrumbs.size) + assertEquals(3, scope.breadcrumbs.size) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt index 1bc88961da4..5613c8eb1f7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt @@ -3,8 +3,8 @@ package io.sentry.android.core import androidx.lifecycle.LifecycleOwner import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.ReplayController import io.sentry.ScopeCallback import io.sentry.SentryLevel @@ -34,7 +34,7 @@ class LifecycleWatcherTest { private class Fixture { val ownerMock = mock() - val hub = mock() + val scopes = mock() val dateProvider = mock() val options = SentryOptions() val replayController = mock() @@ -48,14 +48,14 @@ class LifecycleWatcherTest { val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = mock() whenever(scope.session).thenReturn(session) - whenever(hub.configureScope(argumentCaptor.capture())).thenAnswer { + whenever(scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } options.setReplayController(replayController) - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) return LifecycleWatcher( - hub, + scopes, sessionIntervalMillis, enableAutoSessionTracking, enableAppLifecycleBreadcrumbs, @@ -75,7 +75,7 @@ class LifecycleWatcherTest { fun `if last started session is 0, start new session`() { val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false) watcher.onStart(fixture.ownerMock) - verify(fixture.hub).startSession() + verify(fixture.scopes).startSession() verify(fixture.replayController).start() } @@ -85,7 +85,7 @@ class LifecycleWatcherTest { whenever(fixture.dateProvider.currentTimeMillis).thenReturn(1L, 2L) watcher.onStart(fixture.ownerMock) watcher.onStart(fixture.ownerMock) - verify(fixture.hub, times(2)).startSession() + verify(fixture.scopes, times(2)).startSession() verify(fixture.replayController, times(2)).start() } @@ -95,7 +95,7 @@ class LifecycleWatcherTest { whenever(fixture.dateProvider.currentTimeMillis).thenReturn(2L, 1L) watcher.onStart(fixture.ownerMock) watcher.onStart(fixture.ownerMock) - verify(fixture.hub).startSession() + verify(fixture.scopes).startSession() verify(fixture.replayController).start() } @@ -104,7 +104,7 @@ class LifecycleWatcherTest { val watcher = fixture.getSUT(enableAppLifecycleBreadcrumbs = false) watcher.onStart(fixture.ownerMock) watcher.onStop(fixture.ownerMock) - verify(fixture.hub, timeout(10000)).endSession() + verify(fixture.scopes, timeout(10000)).endSession() verify(fixture.replayController, timeout(10000)).stop() } @@ -119,7 +119,7 @@ class LifecycleWatcherTest { watcher.onStart(fixture.ownerMock) assertNull(watcher.timerTask) - verify(fixture.hub, never()).endSession() + verify(fixture.scopes, never()).endSession() verify(fixture.replayController, never()).stop() } @@ -127,21 +127,21 @@ class LifecycleWatcherTest { fun `When session tracking is disabled, do not start session`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false, enableAppLifecycleBreadcrumbs = false) watcher.onStart(fixture.ownerMock) - verify(fixture.hub, never()).startSession() + verify(fixture.scopes, never()).startSession() } @Test fun `When session tracking is disabled, do not end session`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false, enableAppLifecycleBreadcrumbs = false) watcher.onStop(fixture.ownerMock) - verify(fixture.hub, never()).endSession() + verify(fixture.scopes, never()).endSession() } @Test fun `When app lifecycle breadcrumbs is enabled, add breadcrumb on start`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false) watcher.onStart(fixture.ownerMock) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("app.lifecycle", it.category) assertEquals("navigation", it.type) @@ -155,14 +155,14 @@ class LifecycleWatcherTest { fun `When app lifecycle breadcrumbs is disabled, do not add breadcrumb on start`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false, enableAppLifecycleBreadcrumbs = false) watcher.onStart(fixture.ownerMock) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test fun `When app lifecycle breadcrumbs is enabled, add breadcrumb on stop`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false) watcher.onStop(fixture.ownerMock) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("app.lifecycle", it.category) assertEquals("navigation", it.type) @@ -176,7 +176,7 @@ class LifecycleWatcherTest { fun `When app lifecycle breadcrumbs is disabled, do not add breadcrumb on stop`() { val watcher = fixture.getSUT(enableAutoSessionTracking = false, enableAppLifecycleBreadcrumbs = false) watcher.onStop(fixture.ownerMock) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -186,7 +186,7 @@ class LifecycleWatcherTest { } @Test - fun `if the hub has already a fresh session running, don't start new one`() { + fun `if the scopes has already a fresh session running, don't start new one`() { val watcher = fixture.getSUT( enableAppLifecycleBreadcrumbs = false, session = Session( @@ -195,7 +195,7 @@ class LifecycleWatcherTest { DateUtils.getCurrentDateTime(), 0, "abc", - UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + "3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17", true, 0, 10.0, @@ -208,12 +208,12 @@ class LifecycleWatcherTest { ) watcher.onStart(fixture.ownerMock) - verify(fixture.hub, never()).startSession() + verify(fixture.scopes, never()).startSession() verify(fixture.replayController, never()).start() } @Test - fun `if the hub has a long running session, start new one`() { + fun `if the scopes has a long running session, start new one`() { val watcher = fixture.getSUT( enableAppLifecycleBreadcrumbs = false, session = Session( @@ -222,7 +222,7 @@ class LifecycleWatcherTest { DateUtils.getDateTime(-1), 0, "abc", - UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + "3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17", true, 0, 10.0, @@ -235,7 +235,7 @@ class LifecycleWatcherTest { ) watcher.onStart(fixture.ownerMock) - verify(fixture.hub).startSession() + verify(fixture.scopes).startSession() verify(fixture.replayController).start() } @@ -263,7 +263,7 @@ class LifecycleWatcherTest { DateUtils.getCurrentDateTime(), 0, "abc", - UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + "3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17", true, 0, 10.0, 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 d60f47bd2cc..fd71fa08a85 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 @@ -106,19 +106,6 @@ class ManifestMetadataReaderTest { assertNull(fixture.options.sampleRate) } - @Test - fun `applyMetadata reads session tracking to options`() { - // Arrange - val bundle = bundleOf(ManifestMetadataReader.SESSION_TRACKING_ENABLE to false) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertFalse(fixture.options.isEnableAutoSessionTracking) - } - @Test fun `applyMetadata reads session tracking and keep default value if not found`() { // Arrange @@ -697,45 +684,6 @@ class ManifestMetadataReaderTest { assertNull(fixture.options.tracesSampleRate) } - @Test - fun `applyMetadata reads enableTracing from metadata`() { - // Arrange - val bundle = bundleOf(ManifestMetadataReader.TRACING_ENABLE to true) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertEquals(true, fixture.options.enableTracing) - } - - @Test - fun `applyMetadata does not override enableTracing from options`() { - // Arrange - fixture.options.enableTracing = true - val bundle = bundleOf(ManifestMetadataReader.TRACING_ENABLE to false) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertEquals(true, fixture.options.enableTracing) - } - - @Test - fun `applyMetadata without specifying enableTracing, stays null`() { - // Arrange - val context = fixture.getContext() - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertNull(fixture.options.enableTracing) - } - @Test fun `applyMetadata reads enableAutoActivityLifecycleTracing to options`() { // Arrange @@ -811,31 +759,6 @@ class ManifestMetadataReaderTest { assertTrue(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, fixture.buildInfoProvider) - - // 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, fixture.buildInfoProvider) - - // Assert - assertFalse(fixture.options.isProfilingEnabled) - } - @Test fun `applyMetadata reads profilesSampleRate from metadata`() { // Arrange @@ -890,67 +813,6 @@ class ManifestMetadataReaderTest { assertEquals(listOf("localhost", """^(http|https)://api\..*$"""), fixture.options.tracePropagationTargets) } - @Test - fun `applyMetadata ignores tracingOrigins if tracePropagationTargets is present`() { - // Arrange - val bundle = bundleOf( - ManifestMetadataReader.TRACE_PROPAGATION_TARGETS to """localhost,^(http|https)://api\..*$""", - ManifestMetadataReader.TRACING_ORIGINS to """otherhost""" - ) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertEquals(listOf("localhost", """^(http|https)://api\..*$"""), fixture.options.tracePropagationTargets) - } - - @Test - fun `applyMetadata ignores tracingOrigins if tracePropagationTargets is present even if null`() { - // Arrange - val bundle = bundleOf( - ManifestMetadataReader.TRACE_PROPAGATION_TARGETS to null, - ManifestMetadataReader.TRACING_ORIGINS to """otherhost""" - ) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertTrue(fixture.options.tracePropagationTargets.isEmpty()) - } - - @Test - fun `applyMetadata ignores tracingOrigins if tracePropagationTargets is present even if empty string`() { - // Arrange - val bundle = bundleOf( - ManifestMetadataReader.TRACE_PROPAGATION_TARGETS to "", - ManifestMetadataReader.TRACING_ORIGINS to """otherhost""" - ) - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertTrue(fixture.options.tracePropagationTargets.isEmpty()) - } - - @Test - fun `applyMetadata uses tracingOrigins if tracePropagationTargets is not present`() { - // Arrange - val bundle = bundleOf(ManifestMetadataReader.TRACING_ORIGINS to """otherhost""") - val context = fixture.getContext(metaData = bundle) - - // Act - ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) - - // Assert - assertEquals(listOf("otherhost"), fixture.options.tracePropagationTargets) - } - @Test fun `applyMetadata reads null tracePropagationTargets and sets empty list`() { // Arrange @@ -1348,14 +1210,14 @@ class ManifestMetadataReaderTest { @Test fun `applyMetadata reads performance-v2 flag to options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.ENABLE_PERFORMANCE_V2 to true) + val bundle = bundleOf(ManifestMetadataReader.ENABLE_PERFORMANCE_V2 to false) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.isEnablePerformanceV2) + assertFalse(fixture.options.isEnablePerformanceV2) } @Test @@ -1367,7 +1229,7 @@ class ManifestMetadataReaderTest { ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertFalse(fixture.options.isEnablePerformanceV2) + assertTrue(fixture.options.isEnablePerformanceV2) } @Test @@ -1423,51 +1285,51 @@ class ManifestMetadataReaderTest { } @Test - fun `applyMetadata reads enableMetrics flag to options`() { + fun `applyMetadata does not override replays onErrorSampleRate from options`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.ENABLE_METRICS to true) + val expectedSampleRate = 0.99f + fixture.options.sessionReplay.onErrorSampleRate = expectedSampleRate.toDouble() + val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertTrue(fixture.options.isEnableMetrics) + assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate) } @Test - fun `applyMetadata reads enableMetrics flag to options and keeps default if not found`() { + fun `applyMetadata reads forceInit flag to options`() { // Arrange - val context = fixture.getContext() + val bundle = bundleOf(ManifestMetadataReader.FORCE_INIT to true) + val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertFalse(fixture.options.isEnableMetrics) + assertTrue(fixture.options.isForceInit) } @Test - fun `applyMetadata reads replays onErrorSampleRate from metadata`() { + fun `applyMetadata reads forceInit flag to options and keeps default if not found`() { // Arrange - val expectedSampleRate = 0.99f - - val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate) - val context = fixture.getContext(metaData = bundle) + val context = fixture.getContext() // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate) + assertFalse(fixture.options.isForceInit) } @Test - fun `applyMetadata does not override replays onErrorSampleRate from options`() { + fun `applyMetadata reads replays onErrorSampleRate from metadata`() { // Arrange val expectedSampleRate = 0.99f - fixture.options.sessionReplay.onErrorSampleRate = expectedSampleRate.toDouble() - val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f) + + val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate) val context = fixture.getContext(metaData = bundle) // Act @@ -1517,52 +1379,52 @@ class ManifestMetadataReaderTest { } @Test - fun `applyMetadata reads maxBreadcrumbs to options and sets the value if found`() { + fun `applyMetadata reads integers even when expecting floats`() { // Arrange - val bundle = bundleOf(ManifestMetadataReader.MAX_BREADCRUMBS to 1) + val expectedSampleRate: Int = 1 + + val bundle = bundleOf( + ManifestMetadataReader.SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.TRACES_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.PROFILES_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.REPLAYS_SESSION_SAMPLE_RATE to expectedSampleRate, + ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate + ) val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(1, fixture.options.maxBreadcrumbs) + assertEquals(expectedSampleRate.toDouble(), fixture.options.sampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.tracesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.sessionSampleRate) + assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate) } @Test - fun `applyMetadata reads maxBreadcrumbs to options and keeps default if not found`() { + fun `applyMetadata reads maxBreadcrumbs to options and sets the value if found`() { // Arrange - val context = fixture.getContext() + val bundle = bundleOf(ManifestMetadataReader.MAX_BREADCRUMBS to 1) + val context = fixture.getContext(metaData = bundle) // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(100, fixture.options.maxBreadcrumbs) + assertEquals(1, fixture.options.maxBreadcrumbs) } @Test - fun `applyMetadata reads integers even when expecting floats`() { + fun `applyMetadata reads maxBreadcrumbs to options and keeps default if not found`() { // Arrange - val expectedSampleRate: Int = 1 - - val bundle = bundleOf( - ManifestMetadataReader.SAMPLE_RATE to expectedSampleRate, - ManifestMetadataReader.TRACES_SAMPLE_RATE to expectedSampleRate, - ManifestMetadataReader.PROFILES_SAMPLE_RATE to expectedSampleRate, - ManifestMetadataReader.REPLAYS_SESSION_SAMPLE_RATE to expectedSampleRate, - ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate - ) - val context = fixture.getContext(metaData = bundle) + val context = fixture.getContext() // Act ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) // Assert - assertEquals(expectedSampleRate.toDouble(), fixture.options.sampleRate) - assertEquals(expectedSampleRate.toDouble(), fixture.options.tracesSampleRate) - assertEquals(expectedSampleRate.toDouble(), fixture.options.profilesSampleRate) - assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.sessionSampleRate) - assertEquals(expectedSampleRate.toDouble(), fixture.options.sessionReplay.onErrorSampleRate) + assertEquals(100, fixture.options.maxBreadcrumbs) } } 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 e86de06814b..e282ad71417 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 @@ -1,7 +1,7 @@ package io.sentry.android.core -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.SentryLevel import org.mockito.kotlin.any import org.mockito.kotlin.eq @@ -15,7 +15,7 @@ import kotlin.test.assertTrue class NdkIntegrationTest { private class Fixture { - val hub = mock() + val scopes = mock() val logger = mock() fun getSut(clazz: Class<*>? = SentryNdk::class.java): NdkIntegration { @@ -31,7 +31,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) assertTrue(options.isEnableNdk) @@ -44,7 +44,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) assertTrue(options.isEnableNdk) assertTrue(options.isEnableScopeSync) @@ -62,7 +62,7 @@ class NdkIntegrationTest { val options = getOptions(enableNdk = false) - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) @@ -76,7 +76,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) @@ -90,7 +90,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) @@ -104,7 +104,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) assertTrue(options.isEnableNdk) assertTrue(options.isEnableScopeSync) @@ -122,7 +122,7 @@ class NdkIntegrationTest { val options = getOptions() - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) @@ -136,7 +136,7 @@ class NdkIntegrationTest { val options = getOptions(cacheDir = null) - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger).log(eq(SentryLevel.ERROR), any()) @@ -150,7 +150,7 @@ class NdkIntegrationTest { val options = getOptions(cacheDir = "") - integration.register(fixture.hub, options) + integration.register(fixture.scopes, options) verify(fixture.logger).log(eq(SentryLevel.ERROR), any()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt index c664d986990..fd3c5a4ddce 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt @@ -8,7 +8,7 @@ import android.net.NetworkCapabilities import android.os.Build import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryDateProvider import io.sentry.SentryLevel @@ -42,7 +42,7 @@ class NetworkBreadcrumbsIntegrationTest { private class Fixture { val context = mock() var options = SentryAndroidOptions() - val hub = mock() + val scopes = mock() val mockBuildInfoProvider = mock() val connectivityManager = mock() var nowMs: Long = 0 @@ -79,7 +79,7 @@ class NetworkBreadcrumbsIntegrationTest { fun `When network events breadcrumb is enabled, it registers callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.connectivityManager).registerDefaultNetworkCallback(any()) assertNotNull(sut.networkCallback) @@ -89,7 +89,7 @@ class NetworkBreadcrumbsIntegrationTest { fun `When system events breadcrumb is disabled, it doesn't register callback`() { val sut = fixture.getSut(enableNetworkEventBreadcrumbs = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.connectivityManager, never()).registerDefaultNetworkCallback(any()) assertNull(sut.networkCallback) @@ -101,7 +101,7 @@ class NetworkBreadcrumbsIntegrationTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.M) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.connectivityManager, never()).registerDefaultNetworkCallback(any()) assertNull(sut.networkCallback) @@ -111,7 +111,7 @@ class NetworkBreadcrumbsIntegrationTest { fun `When NetworkBreadcrumbsIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() verify(fixture.connectivityManager).unregisterNetworkCallback(any()) @@ -125,7 +125,7 @@ class NetworkBreadcrumbsIntegrationTest { val sut = fixture.getSut(buildInfo = buildInfo) assertNull(sut.networkCallback) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() verify( @@ -138,12 +138,12 @@ class NetworkBreadcrumbsIntegrationTest { @Test fun `When connected to a new network, a breadcrumb is captured`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(mock()) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("system", it.type) assertEquals("network.event", it.category) @@ -156,27 +156,27 @@ class NetworkBreadcrumbsIntegrationTest { @Test fun `When connected to the same network without disconnecting from the previous one, only one breadcrumb is captured`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) callback.onAvailable(fixture.network) - verify(fixture.hub, times(1)).addBreadcrumb(any()) + verify(fixture.scopes, times(1)).addBreadcrumb(any()) } @Test fun `When disconnected from a network, a breadcrumb is captured`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.scopes).addBreadcrumb(any()) callback.onLost(fixture.network) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("system", it.type) assertEquals("network.event", it.category) @@ -189,12 +189,12 @@ class NetworkBreadcrumbsIntegrationTest { @Test fun `When disconnected from a network, a breadcrumb is captured only if previously connected to that network`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) // callback.onAvailable(network) was not called, so no breadcrumb should be captured callback.onLost(mock()) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -202,7 +202,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -218,7 +218,7 @@ class NetworkBreadcrumbsIntegrationTest { isCellular = false ) ) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("system", it.type) assertEquals("network.event", it.category) @@ -237,18 +237,18 @@ class NetworkBreadcrumbsIntegrationTest { @Test fun `When a network connection detail changes, a breadcrumb is captured only if previously connected to that network`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) // callback.onAvailable(network) was not called, so no breadcrumb should be captured onCapabilitiesChanged(callback, mock()) - verify(fixture.hub, never()).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes, never()).addBreadcrumb(any(), anyOrNull()) } @Test fun `When a network connection detail changes, a new breadcrumb is captured if vpn flag changes`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -259,17 +259,17 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1) onCapabilitiesChanged(callback, details2) onCapabilitiesChanged(callback, details3) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertFalse(it.isVpn) } verifyBreadcrumbInOrder { assertTrue(it.isVpn) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @Test fun `When a network connection detail changes, a new breadcrumb is captured if type changes`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -280,10 +280,10 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1) onCapabilitiesChanged(callback, details2) onCapabilitiesChanged(callback, details3) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals("wifi", it.type) } verifyBreadcrumbInOrder { assertEquals("cellular", it.type) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @@ -292,7 +292,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -303,10 +303,10 @@ class NetworkBreadcrumbsIntegrationTest { // A change of signal strength of 5 doesn't trigger a new breadcrumb onCapabilitiesChanged(callback, details2) onCapabilitiesChanged(callback, details3) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(50, it.signalStrength) } verifyBreadcrumbInOrder { assertEquals(56, it.signalStrength) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @@ -315,7 +315,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -331,11 +331,11 @@ class NetworkBreadcrumbsIntegrationTest { // A change of download bandwidth of 10% (more than 1000) doesn't trigger a new breadcrumb onCapabilitiesChanged(callback, details4) onCapabilitiesChanged(callback, details5) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(1000, it.downBandwidth) } verifyBreadcrumbInOrder { assertEquals(20000, it.downBandwidth) } verifyBreadcrumbInOrder { assertEquals(22001, it.downBandwidth) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @@ -344,7 +344,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -360,18 +360,18 @@ class NetworkBreadcrumbsIntegrationTest { // A change of upload bandwidth of 10% (more than 1000) doesn't trigger a new breadcrumb onCapabilitiesChanged(callback, details4) onCapabilitiesChanged(callback, details5) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(1000, it.upBandwidth) } verifyBreadcrumbInOrder { assertEquals(20000, it.upBandwidth) } verifyBreadcrumbInOrder { assertEquals(22001, it.upBandwidth) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @Test fun `signal strength is 0 if not on Android Q+`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -385,7 +385,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -398,7 +398,7 @@ class NetworkBreadcrumbsIntegrationTest { @Test fun `A breadcrumb is captured when vpn status changes, regardless of the timestamp`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -406,17 +406,17 @@ class NetworkBreadcrumbsIntegrationTest { val details2 = createConnectionDetail(isVpn = true) onCapabilitiesChanged(callback, details1, 0) onCapabilitiesChanged(callback, details2, 0) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertFalse(it.isVpn) } verifyBreadcrumbInOrder { assertTrue(it.isVpn) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @Test fun `A breadcrumb is captured when connection type changes, regardless of the timestamp`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -426,11 +426,11 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1, 0) onCapabilitiesChanged(callback, details2, 0) onCapabilitiesChanged(callback, details3, 0) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals("wifi", it.type) } verifyBreadcrumbInOrder { assertEquals("cellular", it.type) } verifyBreadcrumbInOrder { assertEquals("ethernet", it.type) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @@ -439,7 +439,7 @@ class NetworkBreadcrumbsIntegrationTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) val sut = fixture.getSut(buildInfo = buildInfo) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -449,17 +449,17 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1, 0) onCapabilitiesChanged(callback, details2, 0) onCapabilitiesChanged(callback, details3, 5000) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(1, it.signalStrength) } verifyBreadcrumbInOrder { assertEquals(51, it.signalStrength) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @Test fun `A breadcrumb is captured when downBandwidth changes at most once every 5 seconds`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -469,17 +469,17 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1, 0) onCapabilitiesChanged(callback, details2, 0) onCapabilitiesChanged(callback, details3, 5000) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(1, it.downBandwidth) } verifyBreadcrumbInOrder { assertEquals(2001, it.downBandwidth) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @Test fun `A breadcrumb is captured when upBandwidth changes at most once every 5 seconds`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val callback = sut.networkCallback assertNotNull(callback) callback.onAvailable(fixture.network) @@ -489,10 +489,10 @@ class NetworkBreadcrumbsIntegrationTest { onCapabilitiesChanged(callback, details1, 0) onCapabilitiesChanged(callback, details2, 0) onCapabilitiesChanged(callback, details3, 5000) - inOrder(fixture.hub) { + inOrder(fixture.scopes) { verifyBreadcrumbInOrder { assertEquals(1, it.upBandwidth) } verifyBreadcrumbInOrder { assertEquals(2001, it.upBandwidth) } - verify(fixture.hub, never()).addBreadcrumb(any(), any()) + verify(fixture.scopes, never()).addBreadcrumb(any(), any()) } } @@ -501,7 +501,7 @@ class NetworkBreadcrumbsIntegrationTest { val executor = DeferredExecutorService() val sut = fixture.getSut(executor = executor) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() executor.runAll() @@ -512,7 +512,7 @@ class NetworkBreadcrumbsIntegrationTest { } private fun KInOrder.verifyBreadcrumbInOrder(check: (detail: NetworkBreadcrumbConnectionDetail) -> Unit) { - verify(fixture.hub, times(1)).addBreadcrumb( + verify(fixture.scopes, times(1)).addBreadcrumb( any(), check { val connectionDetail = @@ -523,7 +523,7 @@ class NetworkBreadcrumbsIntegrationTest { } private fun verifyBreadcrumb(check: (detail: NetworkBreadcrumbConnectionDetail) -> Unit) { - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( any(), check { val connectionDetail = 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 12a500966f6..b491dcd088d 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,7 +3,7 @@ package io.sentry.android.core import android.content.ContentProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.MeasurementUnit import io.sentry.SentryTracer import io.sentry.SpanContext @@ -40,7 +40,7 @@ class PerformanceAndroidEventProcessorTest { private class Fixture { val options = SentryAndroidOptions() - val hub = mock() + val scopes = mock() val context = TransactionContext("name", "op", TracesSamplingDecision(true)) lateinit var tracer: SentryTracer val activityFramesTracker = mock() @@ -52,8 +52,8 @@ class PerformanceAndroidEventProcessorTest { AppStartMetrics.getInstance().isAppLaunchedInForeground = true options.tracesSampleRate = tracesSampleRate options.isEnablePerformanceV2 = enablePerformanceV2 - whenever(hub.options).thenReturn(options) - tracer = SentryTracer(context, hub) + whenever(scopes.options).thenReturn(options) + tracer = SentryTracer(context, scopes) return PerformanceAndroidEventProcessor(options, activityFramesTracker) } } @@ -72,7 +72,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ).also { AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) @@ -205,7 +204,7 @@ class PerformanceAndroidEventProcessorTest { fun `add slow and frozen frames if auto transaction`() { val sut = fixture.getSut() val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) val metrics = mapOf( @@ -251,7 +250,7 @@ class PerformanceAndroidEventProcessorTest { // when an activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // and it contains an app.start.cold span @@ -312,7 +311,7 @@ class PerformanceAndroidEventProcessorTest { // when an activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // and it contains an app.start.warm span @@ -358,7 +357,7 @@ class PerformanceAndroidEventProcessorTest { // when an activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // and it contains an app.start.cold span @@ -412,7 +411,7 @@ class PerformanceAndroidEventProcessorTest { // when an activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // and it contains an app.start.cold span @@ -446,7 +445,7 @@ class PerformanceAndroidEventProcessorTest { // when an activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // then the app start metrics should not be attached @@ -475,7 +474,7 @@ class PerformanceAndroidEventProcessorTest { // when the first activity transaction is created val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) @@ -514,7 +513,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) @@ -547,7 +546,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) @@ -580,7 +579,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) // when it contains no app start span and is processed @@ -597,7 +596,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut(enablePerformanceV2 = true) val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) var tr = SentryTransaction(tracer) val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) @@ -618,7 +617,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut() val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) val tr = SentryTransaction(tracer) // given a ttid from 0.0 -> 1.0 @@ -635,7 +634,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -651,7 +649,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) tr.spans.add(ttid) @@ -671,7 +668,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -688,7 +684,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -705,7 +700,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, mutableMapOf( "tag" to "value" ) @@ -742,7 +736,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut() val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) val tr = SentryTransaction(tracer) val span = SentrySpan( @@ -757,7 +751,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -776,7 +769,7 @@ class PerformanceAndroidEventProcessorTest { val sut = fixture.getSut() val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) + val tracer = SentryTracer(context, fixture.scopes) val tr = SentryTransaction(tracer) // given a ttid from 0.0 -> 1.0 @@ -793,7 +786,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -809,7 +801,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) tr.spans.add(ttid) @@ -828,7 +819,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, null ) @@ -845,7 +835,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, mutableMapOf( "thread.name" to "main" ) @@ -864,7 +853,6 @@ class PerformanceAndroidEventProcessorTest { null, emptyMap(), emptyMap(), - null, mutableMapOf( "thread.name" to "background" ) @@ -911,7 +899,7 @@ class PerformanceAndroidEventProcessorTest { AppStartType.UNKNOWN -> "ui.load" } val txn = SentryTransaction(fixture.tracer) - txn.contexts.trace = SpanContext(op, TracesSamplingDecision(false)) + txn.contexts.setTrace(SpanContext(op, TracesSamplingDecision(false))) return txn } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegrationTest.kt index 2b6ca801dae..c764d11c2da 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegrationTest.kt @@ -4,7 +4,7 @@ import android.content.Context import android.telephony.PhoneStateListener import android.telephony.TelephonyManager import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryLevel import io.sentry.test.DeferredExecutorService @@ -41,8 +41,8 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `When system events breadcrumb is enabled, it registers callback`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) verify(fixture.manager).listen(any(), eq(PhoneStateListener.LISTEN_CALL_STATE)) assertNotNull(sut.listener) } @@ -50,8 +50,8 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `Phone state callback is registered in the executorService`() { val sut = fixture.getSut(mock()) - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) assertNull(sut.listener) } @@ -59,9 +59,9 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `When system events breadcrumb is disabled, it doesn't register callback`() { val sut = fixture.getSut() - val hub = mock() + val scopes = mock() sut.register( - hub, + scopes, fixture.options.apply { isEnableSystemEventBreadcrumbs = false } @@ -73,15 +73,15 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `When ActivityBreadcrumbsIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) sut.close() verify(fixture.manager).listen(any(), eq(PhoneStateListener.LISTEN_NONE)) assertNull(sut.listener) } @Test - fun `when hub is closed right after start, integration is not registered`() { + fun `when scopes is closed right after start, integration is not registered`() { val deferredExecutorService = DeferredExecutorService() val sut = fixture.getSut(executorService = deferredExecutorService) sut.register(mock(), fixture.options) @@ -94,11 +94,11 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `When on call state received, added breadcrumb with type and category`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) sut.listener!!.onCallStateChanged(TelephonyManager.CALL_STATE_RINGING, null) - verify(hub).addBreadcrumb( + verify(scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -111,18 +111,18 @@ class PhoneStateBreadcrumbsIntegrationTest { @Test fun `When on idle state received, added breadcrumb with type and category`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) sut.listener!!.onCallStateChanged(TelephonyManager.CALL_STATE_IDLE, null) - verify(hub, never()).addBreadcrumb(any()) + verify(scopes, never()).addBreadcrumb(any()) } @Test fun `When on offhook state received, added breadcrumb with type and category`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) sut.listener!!.onCallStateChanged(TelephonyManager.CALL_STATE_OFFHOOK, null) - verify(hub, never()).addBreadcrumb(any()) + verify(scopes, never()).addBreadcrumb(any()) } } 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 index f75ddbd9019..143b954f743 100644 --- 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 @@ -11,7 +11,7 @@ import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.TypeCheckHint.ANDROID_ACTIVITY import io.sentry.protocol.SentryException -import io.sentry.util.thread.IMainThreadChecker +import io.sentry.util.thread.IThreadChecker import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -35,7 +35,7 @@ class ScreenshotEventProcessorTest { val window = mock() val view = mock() val rootView = mock() - val mainThreadChecker = mock() + val threadChecker = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } @@ -52,12 +52,12 @@ class ScreenshotEventProcessorTest { it.getArgument(0).run() } - whenever(mainThreadChecker.isMainThread).thenReturn(true) + whenever(threadChecker.isMainThread).thenReturn(true) } fun getSut(attachScreenshot: Boolean = false): ScreenshotEventProcessor { options.isAttachScreenshot = attachScreenshot - options.mainThreadChecker = mainThreadChecker + options.threadChecker = threadChecker return ScreenshotEventProcessor(options, buildInfo) } @@ -172,7 +172,7 @@ class ScreenshotEventProcessorTest { @Test fun `when screenshot event processor is called from background thread it executes on main thread`() { val sut = fixture.getSut(true) - whenever(fixture.mainThreadChecker.isMainThread).thenReturn(false) + whenever(fixture.threadChecker.isMainThread).thenReturn(false) CurrentActivityHolder.getInstance().setActivity(fixture.activity) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt index 403f40ee707..f1e345eefce 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt @@ -2,8 +2,8 @@ package io.sentry.android.core import io.sentry.IConnectionStatusProvider import io.sentry.IConnectionStatusProvider.ConnectionStatus -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory @@ -28,7 +28,7 @@ import kotlin.test.Test class SendCachedEnvelopeIntegrationTest { private class Fixture { - val hub: IHub = mock() + val scopes: IScopes = mock() val options = SentryAndroidOptions() val logger = mock() val factory = mock() @@ -74,7 +74,7 @@ class SendCachedEnvelopeIntegrationTest { fun `when cacheDirPath is not set, does nothing`() { val sut = fixture.getSut(cacheDirPath = null) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.factory, never()).create(any(), any()) } @@ -83,7 +83,7 @@ class SendCachedEnvelopeIntegrationTest { fun `when factory returns null, does nothing`() { val sut = fixture.getSut(hasSender = false, mockExecutorService = ImmediateExecutorService()) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.factory).create(any(), any()) verify(fixture.sender, never()).send() @@ -93,7 +93,7 @@ class SendCachedEnvelopeIntegrationTest { fun `when has factory and cacheDirPath set, submits task into queue`() { val sut = fixture.getSut(mockExecutorService = ImmediateExecutorService()) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) await.untilFalse(fixture.flag) verify(fixture.sender).send() @@ -102,7 +102,7 @@ class SendCachedEnvelopeIntegrationTest { @Test fun `when executorService is fake, does nothing`() { val sut = fixture.getSut(mockExecutorService = mock()) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.factory, never()).create(any(), any()) verify(fixture.sender, never()).send() @@ -112,7 +112,7 @@ class SendCachedEnvelopeIntegrationTest { fun `when has startup crash marker, awaits the task on the calling thread`() { val sut = fixture.getSut(hasStartupCrashMarker = true) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // we do not need to await here, because it's executed synchronously verify(fixture.sender).send() @@ -123,7 +123,7 @@ class SendCachedEnvelopeIntegrationTest { val sut = fixture.getSut(hasStartupCrashMarker = true, delaySend = 1000) fixture.options.startupCrashFlushTimeoutMillis = 100 - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // first wait until synchronous send times out and check that the logger was hit in the catch block await.atLeast(500, MILLISECONDS) @@ -144,7 +144,7 @@ class SendCachedEnvelopeIntegrationTest { val connectionStatusProvider = mock() fixture.options.connectionStatusProvider = connectionStatusProvider - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(connectionStatusProvider).addConnectionStatusObserver(any()) } @@ -159,7 +159,7 @@ class SendCachedEnvelopeIntegrationTest { ConnectionStatus.DISCONNECTED ) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.sender, never()).send() } @@ -174,7 +174,7 @@ class SendCachedEnvelopeIntegrationTest { ConnectionStatus.UNKNOWN ) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.factory).create(any(), any()) } @@ -187,7 +187,7 @@ class SendCachedEnvelopeIntegrationTest { whenever(connectionStatusProvider.connectionStatus).thenReturn( ConnectionStatus.DISCONNECTED ) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // when there's no connection no factory create call should be done verify(fixture.sender, never()).send() @@ -215,9 +215,9 @@ class SendCachedEnvelopeIntegrationTest { val rateLimiter = mock { whenever(mock.isActiveForCategory(any())).thenReturn(true) } - whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // no factory call should be done if there's rate limiting active verify(fixture.sender, never()).send() @@ -228,7 +228,7 @@ class SendCachedEnvelopeIntegrationTest { val deferredExecutorService = DeferredExecutorService() val sut = fixture.getSut(mockExecutorService = deferredExecutorService) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.sender, never()).send() sut.close() 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 aa266d5c7a2..efef393dc78 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 @@ -151,16 +151,16 @@ class SentryAndroidOptionsTest { } @Test - fun `performance v2 is disabled by default`() { + fun `performance v2 is enabled by default`() { val sentryOptions = SentryAndroidOptions() - assertFalse(sentryOptions.isEnablePerformanceV2) + assertTrue(sentryOptions.isEnablePerformanceV2) } @Test - fun `performance v2 can be enabled`() { + fun `performance v2 can be disabled`() { val sentryOptions = SentryAndroidOptions() - sentryOptions.isEnablePerformanceV2 = true - assertTrue(sentryOptions.isEnablePerformanceV2) + sentryOptions.isEnablePerformanceV2 = false + assertFalse(sentryOptions.isEnablePerformanceV2) } fun `when options is initialized, enableScopeSync is enabled by default`() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index ec2b3db4ce3..bcba616905f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -15,6 +15,7 @@ import io.sentry.Hint import io.sentry.ILogger import io.sentry.ISentryClient import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryEnvelope import io.sentry.SentryLevel import io.sentry.SentryLevel.DEBUG @@ -40,6 +41,8 @@ import io.sentry.cache.PersistingScopeObserver import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME import io.sentry.cache.PersistingScopeObserver.SCOPE_CACHE import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME +import io.sentry.test.applyTestOptions +import io.sentry.test.initForTest import io.sentry.transport.NoOpEnvelopeCache import io.sentry.util.StringUtils import org.awaitility.kotlin.await @@ -100,9 +103,9 @@ class SentryAndroidTest { } val mockContext = context ?: ContextUtilsTestHelper.mockMetaData(metaData = metadata) when { - logger != null -> SentryAndroid.init(mockContext, logger) - options != null -> SentryAndroid.init(mockContext, options) - else -> SentryAndroid.init(mockContext) + logger != null -> initForTest(mockContext, logger) + options != null -> initForTest(mockContext, options) + else -> initForTest(mockContext) } } @@ -290,7 +293,7 @@ class SentryAndroidTest { val mockContext = ContextUtilsTestHelper.createMockContext(true) val cacheDirPath = Files.createTempDirectory("new_cache").absolutePathString() - SentryAndroid.init(mockContext) { + initForTest(mockContext) { it.dsn = "https://key@sentry.io/123" it.cacheDirPath = cacheDirPath options = it @@ -322,12 +325,13 @@ class SentryAndroidTest { @Test fun `init does not start a session if one is already running`() { val client = mock() + whenever(client.isEnabled).thenReturn(true) initSentryWithForegroundImportance(true, { options -> - options.addIntegration { hub, _ -> - hub.bindClient(client) + options.addIntegration { scopes, _ -> + scopes.bindClient(client) // usually done by LifecycleWatcher - hub.startSession() + scopes.startSession() } }) {} @@ -353,7 +357,7 @@ class SentryAndroidTest { @Test fun `When initializing Sentry a callback is added to application by appStartMetrics`() { val mockContext = ContextUtilsTestHelper.createMockContext(true) - SentryAndroid.init(mockContext) { + initForTest(mockContext) { it.dsn = "https://key@sentry.io/123" } verify(mockContext.applicationContext as Application).registerActivityLifecycleCallbacks(eq(AppStartMetrics.getInstance())) @@ -367,7 +371,7 @@ class SentryAndroidTest { Mockito.mockStatic(ContextUtils::class.java, Mockito.CALLS_REAL_METHODS).use { mockedContextUtils -> mockedContextUtils.`when` { ContextUtils.isForegroundImportance() } .thenReturn(inForeground) - SentryAndroid.init(context) { options -> + initForTest(context) { options -> options.release = "prod" options.dsn = "https://key@sentry.io/123" options.isEnableAutoSessionTracking = true @@ -376,7 +380,7 @@ class SentryAndroidTest { } var session: Session? = null - Sentry.getCurrentHub().configureScope { scope -> + Sentry.getCurrentScopes().configureScope { scope -> session = scope.session } callback(session) @@ -388,7 +392,7 @@ class SentryAndroidTest { fixture.initSut { options -> options.isEnableAutoSessionTracking = false } - Sentry.getCurrentHub().withScope { scope -> + Sentry.getCurrentScopes().withScope { scope -> assertNull(scope.session) } } @@ -424,7 +428,7 @@ class SentryAndroidTest { it.release = "io.sentry.sample@1.1.0+220" it.environment = "debug" // this is necessary to delay the AnrV2Integration processing to execute the configure - // scope block below (otherwise it won't be possible as hub is no-op before .init) + // scope block below (otherwise it won't be possible as scopes is no-op before .init) it.executorService.submit { Sentry.configureScope { scope -> // make sure the scope values changed to test that we're still using previously @@ -557,3 +561,18 @@ class SentryAndroidTest { override fun discard(envelope: SentryEnvelope) = Unit } } + +fun initForTest(context: Context, optionsConfiguration: OptionsConfiguration) { + SentryAndroid.init(context) { + applyTestOptions(it) + optionsConfiguration.configure(it) + } +} + +fun initForTest(context: Context, logger: ILogger) { + SentryAndroid.init(context, logger) +} + +fun initForTest(context: Context) { + SentryAndroid.init(context) +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 1939e7ed801..857cc658c20 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -30,8 +30,8 @@ class SentryLogcatAdapterTest { } val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) when { - options != null -> SentryAndroid.init(mockContext, options) - else -> SentryAndroid.init(mockContext) + options != null -> initForTest(mockContext, options) + else -> initForTest(mockContext) } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index d4469df0715..1fb44774f1e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -119,15 +119,6 @@ class SentryPerformanceProviderTest { verify(fixture.logger, never()).log(any(), any()) } - @Test - fun `when SDK is lower than 21, nothing happens`() { - fixture.getSut(sdkVersion = Build.VERSION_CODES.KITKAT) { config -> - writeConfig(config) - } - assertNull(AppStartMetrics.getInstance().appStartProfiler) - verify(fixture.logger, never()).log(any(), any()) - } - @Test fun `when config file is empty, profiler is not started`() { fixture.getSut { config -> diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index e6d3dfadd7e..f6fb464bf3b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -8,7 +8,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.CheckIn import io.sentry.Hint -import io.sentry.IMetricsAggregator import io.sentry.IScope import io.sentry.ISentryClient import io.sentry.ProfilingTraceData @@ -45,7 +44,7 @@ class SessionTrackingIntegrationTest { @Test fun `session tracking works properly with multiple backgrounds and foregrounds`() { lateinit var options: SentryAndroidOptions - SentryAndroid.init(context) { + initForTest(context) { it.dsn = "https://key@sentry.io/proj" it.release = "io.sentry.samples@2.3.0" it.environment = "production" @@ -184,9 +183,5 @@ class SessionTrackingIntegrationTest { override fun getRateLimiter(): RateLimiter? { TODO("Not yet implemented") } - - override fun getMetricsAggregator(): IMetricsAggregator { - TODO("Not yet implemented") - } } } 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 3dfca15fdb3..45e247a5cb5 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 @@ -5,7 +5,7 @@ import android.content.Intent import android.os.BatteryManager import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryLevel import io.sentry.test.DeferredExecutorService @@ -31,7 +31,7 @@ class SystemEventsBreadcrumbsIntegrationTest { private class Fixture { val context = mock() var options = SentryAndroidOptions() - val hub = mock() + val scopes = mock() fun getSut(enableSystemEventBreadcrumbs: Boolean = true, executorService: ISentryExecutorService = ImmediateExecutorService()): SystemEventsBreadcrumbsIntegration { options = SentryAndroidOptions().apply { @@ -48,7 +48,7 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `When system events breadcrumb is enabled, it registers callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.context).registerReceiver(any(), any()) assertNotNull(sut.receiver) @@ -57,8 +57,8 @@ class SystemEventsBreadcrumbsIntegrationTest { @Test fun `system events callback is registered in the executorService`() { val sut = fixture.getSut(executorService = mock()) - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) assertNull(sut.receiver) } @@ -67,7 +67,7 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `When system events breadcrumb is disabled, it doesn't register callback`() { val sut = fixture.getSut(enableSystemEventBreadcrumbs = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.context, never()).registerReceiver(any(), any()) assertNull(sut.receiver) @@ -77,7 +77,7 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `When ActivityBreadcrumbsIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() verify(fixture.context).unregisterReceiver(any()) @@ -85,10 +85,10 @@ class SystemEventsBreadcrumbsIntegrationTest { } @Test - fun `when hub is closed right after start, integration is not registered`() { + fun `when scopes is closed right after start, integration is not registered`() { val deferredExecutorService = DeferredExecutorService() val sut = fixture.getSut(executorService = deferredExecutorService) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNull(sut.receiver) sut.close() deferredExecutorService.runAll() @@ -99,13 +99,13 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `When broadcast received, added breadcrumb with type and category`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val intent = Intent().apply { action = Intent.ACTION_TIME_CHANGED } sut.receiver!!.onReceive(fixture.context, intent) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -120,7 +120,7 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `handles battery changes`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val intent = Intent().apply { action = Intent.ACTION_BATTERY_CHANGED putExtra(BatteryManager.EXTRA_LEVEL, 75) @@ -129,7 +129,7 @@ class SystemEventsBreadcrumbsIntegrationTest { } sut.receiver!!.onReceive(fixture.context, intent) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -145,7 +145,7 @@ class SystemEventsBreadcrumbsIntegrationTest { fun `battery changes are debounced`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) val intent1 = Intent().apply { action = Intent.ACTION_BATTERY_CHANGED putExtra(BatteryManager.EXTRA_LEVEL, 80) @@ -161,14 +161,14 @@ class SystemEventsBreadcrumbsIntegrationTest { sut.receiver!!.onReceive(fixture.context, intent2) // should only add the first crumb - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals(it.data["level"], 80f) assertEquals(it.data["charging"], false) }, anyOrNull() ) - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) } @Test @@ -176,7 +176,7 @@ class SystemEventsBreadcrumbsIntegrationTest { val sut = fixture.getSut() whenever(fixture.context.registerReceiver(any(), any())).thenThrow(SecurityException()) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertFalse(fixture.options.isEnableSystemEventBreadcrumbs) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/TempSensorBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/TempSensorBreadcrumbsIntegrationTest.kt index d443b1e3458..5d049e3dada 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/TempSensorBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/TempSensorBreadcrumbsIntegrationTest.kt @@ -7,7 +7,7 @@ import android.hardware.SensorEventListener import android.hardware.SensorManager import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryLevel import io.sentry.TypeCheckHint @@ -47,8 +47,8 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `When system events breadcrumb is enabled, it registers callback`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) verify(fixture.manager).registerListener(any(), any(), eq(SensorManager.SENSOR_DELAY_NORMAL)) assertNotNull(sut.sensorManager) } @@ -56,8 +56,8 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `temp sensor listener is registered in the executorService`() { val sut = fixture.getSut(executorService = mock()) - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) assertNull(sut.sensorManager) } @@ -65,9 +65,9 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `When system events breadcrumb is disabled, it should not register a callback`() { val sut = fixture.getSut() - val hub = mock() + val scopes = mock() sut.register( - hub, + scopes, fixture.options.apply { isEnableSystemEventBreadcrumbs = false } @@ -79,15 +79,15 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `When TempSensorBreadcrumbsIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) sut.close() verify(fixture.manager).unregisterListener(any()) assertNull(sut.sensorManager) } @Test - fun `when hub is closed right after start, integration is not registered`() { + fun `when scopes is closed right after start, integration is not registered`() { val deferredExecutorService = DeferredExecutorService() val sut = fixture.getSut(executorService = deferredExecutorService) sut.register(mock(), fixture.options) @@ -100,14 +100,14 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `When onSensorChanged received, add a breadcrumb with type and category`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) val sensorCtor = "android.hardware.SensorEvent".getDeclaredCtor(emptyArray()) val sensorEvent: SensorEvent = sensorCtor.newInstance() as SensorEvent sensorEvent.injectForField("values", FloatArray(2) { 1F }) sut.onSensorChanged(sensorEvent) - verify(hub).addBreadcrumb( + verify(scopes).addBreadcrumb( check { assertEquals("device.event", it.category) assertEquals("system", it.type) @@ -122,12 +122,12 @@ class TempSensorBreadcrumbsIntegrationTest { @Test fun `When onSensorChanged received and null values, do not add a breadcrumb`() { val sut = fixture.getSut() - val hub = mock() - sut.register(hub, fixture.options) + val scopes = mock() + sut.register(scopes, fixture.options) val event = mock() assertNull(event.values) sut.onSensorChanged(event) - verify(hub, never()).addBreadcrumb(any()) + verify(scopes, never()).addBreadcrumb(any()) } } 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 d43dfe14197..379e3db3532 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 @@ -7,7 +7,7 @@ import android.content.res.Resources import android.util.DisplayMetrics import android.view.Window import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.Hub +import io.sentry.Scopes import io.sentry.android.core.internal.gestures.NoOpWindowCallback import io.sentry.android.core.internal.gestures.SentryWindowCallback import org.junit.runner.RunWith @@ -26,7 +26,7 @@ class UserInteractionIntegrationTest { private class Fixture { val application = mock() - val hub = mock() + val scopes = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } @@ -39,7 +39,7 @@ class UserInteractionIntegrationTest { isAndroidXAvailable: Boolean = true ): UserInteractionIntegration { whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) whenever(window.callback).thenReturn(callback) whenever(activity.window).thenReturn(window) @@ -65,7 +65,7 @@ class UserInteractionIntegrationTest { @Test fun `when user interaction breadcrumb is enabled registers a callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application).registerActivityLifecycleCallbacks(any()) } @@ -75,7 +75,7 @@ class UserInteractionIntegrationTest { val sut = fixture.getSut() fixture.options.isEnableUserInteractionBreadcrumbs = false - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application, never()).registerActivityLifecycleCallbacks(any()) } @@ -83,7 +83,7 @@ class UserInteractionIntegrationTest { @Test fun `when UserInteractionIntegration is closed unregisters the callback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() @@ -94,7 +94,7 @@ class UserInteractionIntegrationTest { fun `when androidx is unavailable doesn't register a callback`() { val sut = fixture.getSut(isAndroidXAvailable = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application, never()).registerActivityLifecycleCallbacks(any()) } @@ -102,7 +102,7 @@ class UserInteractionIntegrationTest { @Test fun `registers window callback on activity resumed`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityResumed(fixture.activity) @@ -114,7 +114,7 @@ class UserInteractionIntegrationTest { @Test fun `when no original callback delegates to NoOpWindowCallback`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityResumed(fixture.activity) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ViewHierarchyEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ViewHierarchyEventProcessorTest.kt index 3f17d8bff27..3c72af480eb 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ViewHierarchyEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ViewHierarchyEventProcessorTest.kt @@ -12,7 +12,7 @@ import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.TypeCheckHint import io.sentry.protocol.SentryException -import io.sentry.util.thread.IMainThreadChecker +import io.sentry.util.thread.IThreadChecker import org.junit.runner.RunWith import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.any @@ -46,7 +46,7 @@ class ViewHierarchyEventProcessorTest { } } val activity = mock() - val mainThreadChecker = mock() + val threadChecker = mock() val window = mock() val view = mock() val options = SentryAndroidOptions().apply { @@ -62,14 +62,14 @@ class ViewHierarchyEventProcessorTest { whenever(activity.runOnUiThread(any())).then { it.getArgument(0).run() } - whenever(mainThreadChecker.isMainThread).thenReturn(true) + whenever(threadChecker.isMainThread).thenReturn(true) CurrentActivityHolder.getInstance().setActivity(activity) } fun getSut(attachViewHierarchy: Boolean = false): ViewHierarchyEventProcessor { options.isAttachViewHierarchy = attachViewHierarchy - options.mainThreadChecker = mainThreadChecker + options.threadChecker = threadChecker return ViewHierarchyEventProcessor(options) } @@ -96,7 +96,7 @@ class ViewHierarchyEventProcessorTest { fun `should return a view hierarchy as byte array`() { val viewHierarchy = ViewHierarchyEventProcessor.snapshotViewHierarchyAsData( fixture.activity, - fixture.mainThreadChecker, + fixture.threadChecker, fixture.serializer, fixture.logger ) @@ -109,7 +109,7 @@ class ViewHierarchyEventProcessorTest { fun `should return null as bytes are empty array`() { val viewHierarchy = ViewHierarchyEventProcessor.snapshotViewHierarchyAsData( fixture.activity, - fixture.mainThreadChecker, + fixture.threadChecker, fixture.emptySerializer, fixture.logger ) @@ -161,7 +161,7 @@ class ViewHierarchyEventProcessorTest { @Test fun `when an event errored in the background, the view hierarchy should captured on the main thread`() { - whenever(fixture.mainThreadChecker.isMainThread).thenReturn(false) + whenever(fixture.threadChecker.isMainThread).thenReturn(false) val (event, hint) = fixture.process( true, diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt index ba70e5a8797..fc294d2481c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/cache/AndroidEnvelopeCacheTest.kt @@ -51,6 +51,7 @@ class AndroidEnvelopeCacheTest { AppStartMetrics.getInstance().apply { if (options.isEnablePerformanceV2) { appStartTimeSpan.setStartedAt(appStartMillis) + sdkInitTimeSpan.setStartedAt(appStartMillis) } else { sdkInitTimeSpan.setStartedAt(appStartMillis) } 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 1e6652276a7..74edfb43025 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 @@ -10,8 +10,8 @@ import android.view.Window import android.widget.CheckBox import android.widget.RadioButton import io.sentry.Breadcrumb -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.PropagationContext import io.sentry.Scope.IWithPropagationContext import io.sentry.ScopeCallback @@ -40,7 +40,7 @@ class SentryGestureListenerClickTest { gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) dsn = "https://key@sentry.io/proj" } - val hub = mock() + val scopes = mock() val scope = mock() val propagationContext = PropagationContext() lateinit var target: View @@ -86,11 +86,11 @@ class SentryGestureListenerClickTest { whenever(context.resources).thenReturn(resources) whenever(this.target.context).thenReturn(context) whenever(activity.window).thenReturn(window) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) doAnswer { (it.arguments[0] as IWithPropagationContext).accept(propagationContext); propagationContext; }.whenever(scope).withPropagationContext(any()) return SentryGestureListener( activity, - hub, + scopes, options ) } @@ -123,7 +123,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("ui.click", it.category) assertEquals("user", it.type) @@ -146,7 +146,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("radio_button", it.data["view.id"]) assertEquals("android.widget.RadioButton", it.data["view.class"]) @@ -166,7 +166,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("check_box", it.data["view.id"]) assertEquals("android.widget.CheckBox", it.data["view.class"]) @@ -185,7 +185,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -198,7 +198,7 @@ class SentryGestureListenerClickTest { val sut = fixture.getSut(event, "decor_view", targetOverride = decorView) sut.onSingleTapUp(event) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals(decorView.javaClass.canonicalName, it.data["view.class"]) assertEquals("decor_view", it.data["view.id"]) @@ -214,7 +214,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -230,7 +230,7 @@ class SentryGestureListenerClickTest { sut.onSingleTapUp(event) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals(fixture.target.javaClass.simpleName, it.data["view.class"]) }, 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 5d39b647530..e5a9623c4d3 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 @@ -11,8 +11,8 @@ import android.widget.AbsListView import android.widget.ListAdapter import androidx.core.view.ScrollingView import io.sentry.Breadcrumb -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.PropagationContext import io.sentry.Scope import io.sentry.ScopeCallback @@ -44,7 +44,7 @@ class SentryGestureListenerScrollTest { isEnableUserInteractionTracing = true gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) } - val hub = mock() + val scopes = mock() val scope = mock() val propagationContext = PropagationContext() @@ -77,11 +77,11 @@ class SentryGestureListenerScrollTest { endEvent.mockDirection(firstEvent, direction) } whenever(activity.window).thenReturn(window) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) doAnswer { (it.arguments[0] as Scope.IWithPropagationContext).accept(propagationContext); propagationContext }.whenever(scope).withPropagationContext(any()) return SentryGestureListener( activity, - hub, + scopes, options ) } @@ -99,7 +99,7 @@ class SentryGestureListenerScrollTest { } sut.onUp(fixture.endEvent) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("ui.scroll", it.category) assertEquals("user", it.type) @@ -122,7 +122,7 @@ class SentryGestureListenerScrollTest { } sut.onUp(fixture.endEvent) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -143,8 +143,8 @@ class SentryGestureListenerScrollTest { sut.onFling(fixture.firstEvent, fixture.endEvent, 1.0f, 1.0f) sut.onUp(fixture.endEvent) - inOrder(fixture.hub) { - verify(fixture.hub).addBreadcrumb( + inOrder(fixture.scopes) { + verify(fixture.scopes).addBreadcrumb( check { assertEquals("ui.swipe", it.category) assertEquals("user", it.type) @@ -155,8 +155,8 @@ class SentryGestureListenerScrollTest { }, anyOrNull() ) - verify(fixture.hub).configureScope(anyOrNull()) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).configureScope(anyOrNull()) + verify(fixture.scopes).addBreadcrumb( check { assertEquals("ui.swipe", it.category) assertEquals("user", it.type) @@ -168,7 +168,7 @@ class SentryGestureListenerScrollTest { anyOrNull() ) } - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) } @Test @@ -177,7 +177,7 @@ class SentryGestureListenerScrollTest { sut.onUp(fixture.firstEvent) sut.onDown(fixture.endEvent) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test @@ -190,7 +190,7 @@ class SentryGestureListenerScrollTest { } sut.onUp(fixture.endEvent) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index c7ada69c88e..07dde15e8f1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -9,8 +9,8 @@ import android.view.ViewGroup import android.view.Window import android.widget.AbsListView import android.widget.ListAdapter -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryTracer @@ -23,6 +23,7 @@ import io.sentry.TransactionOptions import io.sentry.android.core.SentryAndroidOptions import io.sentry.protocol.SentryId import io.sentry.protocol.TransactionNameSource +import org.mockito.ArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.clearInvocations @@ -46,9 +47,10 @@ class SentryGestureListenerTracingTest { val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } - val hub = mock() + val scopes = mock() val event = mock() val scope = mock() + val transactionOptionsArgumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(TransactionOptions::class.java) lateinit var target: View lateinit var transaction: SentryTracer @@ -64,9 +66,9 @@ class SentryGestureListenerTracingTest { options.isEnableUserInteractionBreadcrumbs = true options.gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) - this.transaction = transaction ?: SentryTracer(TransactionContext("name", "op"), hub) + this.transaction = transaction ?: SentryTracer(TransactionContext("name", "op"), scopes) target = mockView(event = event, clickable = true, context = context) window.mockDecorView(event = event, context = context) { @@ -85,14 +87,13 @@ class SentryGestureListenerTracingTest { whenever(target.context).thenReturn(context) whenever(activity.window).thenReturn(window) - - whenever(hub.startTransaction(any(), any())) + whenever(scopes.startTransaction(any(), transactionOptionsArgumentCaptor.capture())) .thenReturn(this.transaction) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) return SentryGestureListener( activity, - hub, + scopes, options ) } @@ -106,7 +107,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -118,7 +119,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -130,7 +131,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -140,7 +141,7 @@ class SentryGestureListenerTracingTest { fun `when transaction is created, set transaction to the bound Scope`() { val sut = fixture.getSut() - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) sut.applyScope(scope, fixture.transaction) @@ -155,9 +156,9 @@ class SentryGestureListenerTracingTest { fun `when transaction is created, do not overwrite transaction already bound to the Scope`() { val sut = fixture.getSut() - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) - val previousTransaction = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val previousTransaction = SentryTracer(TransactionContext("name", "op"), fixture.scopes) scope.transaction = previousTransaction sut.applyScope(scope, fixture.transaction) @@ -173,14 +174,14 @@ class SentryGestureListenerTracingTest { val sut = fixture.getSut() val expectedStatus = SpanStatus.CANCELLED - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) sut.applyScope(scope, fixture.transaction) } sut.onSingleTapUp(fixture.event) - whenever(fixture.hub.configureScope(any())).thenAnswer { + whenever(fixture.scopes.configureScope(any())).thenAnswer { val scope = Scope(fixture.options) scope.transaction = fixture.transaction @@ -199,7 +200,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity.test_button", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -214,7 +215,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { transactionOptions -> assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) @@ -232,7 +233,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("ui.action.click", it.operation) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -248,7 +249,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity.test_button", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -256,7 +257,7 @@ class SentryGestureListenerTracingTest { any() ) - clearInvocations(fixture.hub) + clearInvocations(fixture.scopes) // second view interaction with another view val newTarget = mockView(event = fixture.event, clickable = true, context = fixture.context) val newContext = mock() @@ -269,16 +270,16 @@ class SentryGestureListenerTracingTest { whenever(it.getChildAt(0)).thenReturn(newTarget) } - whenever(fixture.hub.startTransaction(any(), any())) + whenever(fixture.scopes.startTransaction(any(), 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) + SentryTracer(TransactionContext("name", "op"), fixture.scopes) } sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity.test_checkbox", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -293,7 +294,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity.test_scroll_view", it.name) assertEquals("ui.action.click", it.operation) @@ -302,20 +303,20 @@ class SentryGestureListenerTracingTest { any() ) - clearInvocations(fixture.hub) + clearInvocations(fixture.scopes) // second view interaction with a different interaction type (scroll) - whenever(fixture.hub.startTransaction(any(), any())) + whenever(fixture.scopes.startTransaction(any(), 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) + SentryTracer(TransactionContext("name", "op"), fixture.scopes) } sut.onScroll(fixture.event, mock(), 10.0f, 0f) sut.onUp(mock()) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("Activity.test_scroll_view", it.name) assertEquals("ui.action.scroll", it.operation) @@ -340,7 +341,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) // then two transaction should be captured - verify(fixture.hub, times(2)).startTransaction( + verify(fixture.scopes, times(2)).startTransaction( check { assertEquals("Activity.test_button", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) @@ -355,7 +356,7 @@ class SentryGestureListenerTracingTest { sut.onSingleTapUp(fixture.event) - assertEquals("auto.ui.gesture_listener.old_view_system", fixture.transaction.spanContext.origin) + assertEquals("auto.ui.gesture_listener.old_view_system", fixture.transactionOptionsArgumentCaptor.value.origin) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidMainThreadCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt similarity index 72% rename from sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidMainThreadCheckerTest.kt rename to sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt index c759bdf79e0..eb59f0732e1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidMainThreadCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt @@ -8,23 +8,23 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) -class AndroidMainThreadCheckerTest { +class AndroidThreadCheckerTest { @Test fun `When calling isMainThread from the same thread, it should return true`() { - assertTrue(AndroidMainThreadChecker.getInstance().isMainThread) + assertTrue(AndroidThreadChecker.getInstance().isMainThread) } @Test fun `When calling isMainThread with the current thread, it should return true`() { val thread = Thread.currentThread() - assertTrue(AndroidMainThreadChecker.getInstance().isMainThread(thread)) + assertTrue(AndroidThreadChecker.getInstance().isMainThread(thread)) } @Test fun `When calling isMainThread from a different thread, it should return false`() { val thread = Thread() - assertFalse(AndroidMainThreadChecker.getInstance().isMainThread(thread)) + assertFalse(AndroidThreadChecker.getInstance().isMainThread(thread)) } @Test @@ -33,7 +33,7 @@ class AndroidMainThreadCheckerTest { val sentryThread = SentryThread().apply { id = thread.id } - assertTrue(AndroidMainThreadChecker.getInstance().isMainThread(sentryThread)) + assertTrue(AndroidThreadChecker.getInstance().isMainThread(sentryThread)) } @Test @@ -42,6 +42,6 @@ class AndroidMainThreadCheckerTest { val sentryThread = SentryThread().apply { id = thread.id } - assertFalse(AndroidMainThreadChecker.getInstance().isMainThread(sentryThread)) + assertFalse(AndroidThreadChecker.getInstance().isMainThread(sentryThread)) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt index cb0602f016b..4ae35c05071 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt @@ -2,7 +2,7 @@ package io.sentry.android.core.performance import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryNanotimeDate import io.sentry.SentryOptions @@ -28,18 +28,17 @@ import kotlin.test.assertTrue class ActivityLifecycleSpanHelperTest { private class Fixture { val appStartSpan: ISpan - val hub = mock() + val scopes = mock() val options = SentryOptions() val date = SentryNanotimeDate(Date(1), 1000000) val endDate = SentryNanotimeDate(Date(3), 3000000) init { - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) appStartSpan = Span( TransactionContext("name", "op", TracesSamplingDecision(true)), - SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), hub), - hub, - null, + SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), scopes), + scopes, SpanOptions() ) } diff --git a/sentry-android-fragment/api/sentry-android-fragment.api b/sentry-android-fragment/api/sentry-android-fragment.api index d850abedca3..044d9cfbf54 100644 --- a/sentry-android-fragment/api/sentry-android-fragment.api +++ b/sentry-android-fragment/api/sentry-android-fragment.api @@ -18,7 +18,7 @@ public final class io/sentry/android/fragment/FragmentLifecycleIntegration : and 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 register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/fragment/FragmentLifecycleState : java/lang/Enum { @@ -45,9 +45,9 @@ public final class io/sentry/android/fragment/FragmentLifecycleState$Companion { public final class io/sentry/android/fragment/SentryFragmentLifecycleCallbacks : androidx/fragment/app/FragmentManager$FragmentLifecycleCallbacks { public static final field Companion Lio/sentry/android/fragment/SentryFragmentLifecycleCallbacks$Companion; public static final field FRAGMENT_LOAD_OP Ljava/lang/String; - public fun (Lio/sentry/IHub;Ljava/util/Set;Z)V - public synthetic fun (Lio/sentry/IHub;Ljava/util/Set;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/IHub;ZZ)V + public fun (Lio/sentry/IScopes;Ljava/util/Set;Z)V + public synthetic fun (Lio/sentry/IScopes;Ljava/util/Set;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;ZZ)V public fun (ZZ)V public synthetic fun (ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getEnableAutoFragmentLifecycleTracing ()Z diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index bd94c58684d..f1c44422a58 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -5,7 +5,7 @@ import android.app.Application import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle import androidx.fragment.app.FragmentActivity -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Integration import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG @@ -40,11 +40,11 @@ class FragmentLifecycleIntegration( enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing ) - private lateinit var hub: IHub + private lateinit var scopes: IScopes private lateinit var options: SentryOptions - override fun register(hub: IHub, options: SentryOptions) { - this.hub = hub + override fun register(scopes: IScopes, options: SentryOptions) { + this.scopes = scopes this.options = options application.registerActivityLifecycleCallbacks(this) @@ -66,7 +66,7 @@ class FragmentLifecycleIntegration( ?.supportFragmentManager ?.registerFragmentLifecycleCallbacks( SentryFragmentLifecycleCallbacks( - hub = hub, + scopes = scopes, filterFragmentLifecycleBreadcrumbs = filterFragmentLifecycleBreadcrumbs, enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing ), 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 983d17464bd..cf5b14b43c0 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 @@ -8,9 +8,9 @@ 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.IScopes import io.sentry.ISpan +import io.sentry.ScopesAdapter import io.sentry.SentryLevel.INFO import io.sentry.SpanStatus import io.sentry.TypeCheckHint.ANDROID_FRAGMENT @@ -20,17 +20,17 @@ private const val TRACE_ORIGIN = "auto.ui.fragment" @Suppress("TooManyFunctions") class SentryFragmentLifecycleCallbacks( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), val filterFragmentLifecycleBreadcrumbs: Set, val enableAutoFragmentLifecycleTracing: Boolean ) : FragmentLifecycleCallbacks() { constructor( - hub: IHub, + scopes: IScopes, enableFragmentLifecycleBreadcrumbs: Boolean, enableAutoFragmentLifecycleTracing: Boolean ) : this( - hub = hub, + scopes = scopes, filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states .takeIf { enableFragmentLifecycleBreadcrumbs } .orEmpty(), @@ -41,14 +41,14 @@ class SentryFragmentLifecycleCallbacks( enableFragmentLifecycleBreadcrumbs: Boolean = true, enableAutoFragmentLifecycleTracing: Boolean = false ) : this( - hub = HubAdapter.getInstance(), + scopes = ScopesAdapter.getInstance(), filterFragmentLifecycleBreadcrumbs = FragmentLifecycleState.states .takeIf { enableFragmentLifecycleBreadcrumbs } .orEmpty(), enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing ) - private val isPerformanceEnabled get() = hub.options.isTracingEnabled && enableAutoFragmentLifecycleTracing + private val isPerformanceEnabled get() = scopes.options.isTracingEnabled && enableAutoFragmentLifecycleTracing private val fragmentsWithOngoingTransactions = WeakHashMap() @@ -81,8 +81,8 @@ class SentryFragmentLifecycleCallbacks( // we only start the tracing for the fragment if the fragment has been added to its activity // and not only to the backstack if (fragment.isAdded) { - if (hub.options.isEnableScreenTracking) { - hub.configureScope { it.screen = getFragmentName(fragment) } + if (scopes.options.isEnableScreenTracking) { + scopes.configureScope { it.screen = getFragmentName(fragment) } } startTracing(fragment) } @@ -145,7 +145,7 @@ class SentryFragmentLifecycleCallbacks( val hint = Hint() .also { it.set(ANDROID_FRAGMENT, fragment) } - hub.addBreadcrumb(breadcrumb, hint) + scopes.addBreadcrumb(breadcrumb, hint) } private fun getFragmentName(fragment: Fragment): String { @@ -161,7 +161,7 @@ class SentryFragmentLifecycleCallbacks( } var transaction: ISpan? = null - hub.configureScope { + scopes.configureScope { transaction = it.transaction } 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 032aef58e1d..84286503b93 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 @@ -4,7 +4,7 @@ import android.app.Activity import android.app.Application import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import org.mockito.kotlin.check import org.mockito.kotlin.doReturn @@ -24,14 +24,14 @@ class FragmentLifecycleIntegrationTest { val fragmentActivity = mock { on { supportFragmentManager } doReturn fragmentManager } - val hub = mock() + val scopes = mock() val options = SentryOptions() fun getSut( enableFragmentLifecycleBreadcrumbs: Boolean = true, enableAutoFragmentLifecycleTracing: Boolean = false ): FragmentLifecycleIntegration { - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) return FragmentLifecycleIntegration( application = application, enableFragmentLifecycleBreadcrumbs = enableFragmentLifecycleBreadcrumbs, @@ -46,7 +46,7 @@ class FragmentLifecycleIntegrationTest { fun `When register, it should register activity lifecycle callbacks`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.application).registerActivityLifecycleCallbacks(sut) } @@ -55,7 +55,7 @@ class FragmentLifecycleIntegrationTest { fun `When close, it should unregister lifecycle callbacks`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.close() verify(fixture.application).unregisterActivityLifecycleCallbacks(sut) @@ -69,7 +69,7 @@ class FragmentLifecycleIntegrationTest { on { supportFragmentManager } doReturn fragmentManager } - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(fragmentActivity, savedInstanceState = null) verify(fragmentManager).registerFragmentLifecycleCallbacks( @@ -84,7 +84,7 @@ class FragmentLifecycleIntegrationTest { fun `When FragmentActivity is created, it should register fragment lifecycle callbacks with passed config`() { val sut = fixture.getSut(enableFragmentLifecycleBreadcrumbs = false, enableAutoFragmentLifecycleTracing = true) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(fixture.fragmentActivity, savedInstanceState = null) verify(fixture.fragmentManager).registerFragmentLifecycleCallbacks( @@ -102,7 +102,7 @@ class FragmentLifecycleIntegrationTest { val sut = fixture.getSut() val activity = mock() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.onActivityCreated(activity, savedInstanceState = null) } 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 249d40d299d..91f84c5af19 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,8 +5,8 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import io.sentry.Breadcrumb -import io.sentry.Hub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.ScopeCallback @@ -32,7 +32,7 @@ class SentryFragmentLifecycleCallbacksTest { private class Fixture { val fragmentManager = mock() - val hub = mock() + val scopes = mock() val fragment = mock() val context = mock() val scope = mock() @@ -45,7 +45,7 @@ class SentryFragmentLifecycleCallbacksTest { tracesSampleRate: Double? = 1.0, isAdded: Boolean = true ): SentryFragmentLifecycleCallbacks { - whenever(hub.options).thenReturn( + whenever(scopes.options).thenReturn( SentryOptions().apply { setTracesSampleRate(tracesSampleRate) } @@ -53,14 +53,14 @@ class SentryFragmentLifecycleCallbacksTest { whenever(span.spanContext).thenReturn( SpanContext(SentryId.EMPTY_ID, SpanId.EMPTY_ID, "op", null, null) ) - whenever(transaction.startChild(any(), any())).thenReturn(span) + whenever(transaction.startChild(any(), any())).thenReturn(span) whenever(scope.transaction).thenReturn(transaction) - whenever(hub.configureScope(any())).thenAnswer { + whenever(scopes.configureScope(any())).thenAnswer { (it.arguments[0] as ScopeCallback).run(scope) } whenever(fragment.isAdded).thenReturn(isAdded) return SentryFragmentLifecycleCallbacks( - hub = hub, + scopes = scopes, filterFragmentLifecycleBreadcrumbs = loggedFragmentLifecycleStates, enableAutoFragmentLifecycleTracing = enableAutoFragmentLifecycleTracing ) @@ -190,7 +190,7 @@ class SentryFragmentLifecycleCallbacksTest { sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) - verify(fixture.transaction, never()).startChild(any(), any()) + verify(fixture.transaction, never()).startChild(any(), any()) } @Test @@ -200,10 +200,10 @@ class SentryFragmentLifecycleCallbacksTest { sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) verify(fixture.transaction).startChild( - check { + check { assertEquals(SentryFragmentLifecycleCallbacks.FRAGMENT_LOAD_OP, it) }, - check { + check { assertEquals("androidx.fragment.app.Fragment", it) } ) @@ -215,7 +215,7 @@ class SentryFragmentLifecycleCallbacksTest { sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) - verify(fixture.transaction, never()).startChild(any(), any()) + verify(fixture.transaction, never()).startChild(any(), any()) } @Test @@ -225,7 +225,7 @@ class SentryFragmentLifecycleCallbacksTest { sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) sut.onFragmentCreated(fixture.fragmentManager, fixture.fragment, savedInstanceState = null) - verify(fixture.transaction).startChild(any(), any()) + verify(fixture.transaction).startChild(any(), any()) } @Test @@ -272,7 +272,7 @@ class SentryFragmentLifecycleCallbacksTest { } private fun verifyBreadcrumbAdded(expectedState: String) { - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { breadcrumb: Breadcrumb -> assertEquals("ui.fragment.lifecycle", breadcrumb.category) assertEquals("navigation", breadcrumb.type) @@ -285,6 +285,6 @@ class SentryFragmentLifecycleCallbacksTest { } private fun verifyBreadcrumbAddedCount(count: Int) { - verify(fixture.hub, times(count)).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes, times(count)).addBreadcrumb(any(), anyOrNull()) } } diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts index 9cd769676a1..b15e7626dc6 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts @@ -15,7 +15,7 @@ android { defaultConfig { applicationId = "io.sentry.uitest.android.benchmark" - minSdk = Config.Android.minSdkVersionNdk + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 1 versionName = "1.0.0" @@ -99,6 +99,7 @@ dependencies { errorprone(Config.CompileOnly.errorprone) errorprone(Config.CompileOnly.errorProneNullAway) + androidTestImplementation(projects.sentryTestSupport) androidTestImplementation(Config.TestLibs.kotlinTestJunit) androidTestImplementation(Config.TestLibs.espressoCore) androidTestImplementation(Config.TestLibs.androidxTestCoreKtx) 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 index 110f9214b51..ca6ed2ded2d 100644 --- 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 @@ -1,5 +1,6 @@ package io.sentry.uitest.android.benchmark +import android.content.Context import android.os.Bundle import androidx.lifecycle.Lifecycle import androidx.test.core.app.launchActivity @@ -12,8 +13,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.runner.AndroidJUnitRunner import io.sentry.ITransaction import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryOptions import io.sentry.android.core.SentryAndroid +import io.sentry.android.core.SentryAndroidOptions +import io.sentry.test.applyTestOptions import io.sentry.uitest.android.benchmark.util.BenchmarkOperation import org.junit.runner.RunWith import kotlin.test.AfterTest @@ -62,7 +66,7 @@ class SentryBenchmarkTest : BaseBenchmarkTest() { choreographer, before = { runner.runOnMainSync { - SentryAndroid.init(context) { options: SentryOptions -> + initForTest(context) { options: SentryOptions -> options.dsn = "https://key@uri/1234567" options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 @@ -127,3 +131,10 @@ class SentryBenchmarkTest : BaseBenchmarkTest() { } } } + +fun initForTest(context: Context, optionsConfiguration: OptionsConfiguration) { + SentryAndroid.init(context) { + applyTestOptions(it) + optionsConfiguration.configure(it) + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts index cebf744a24d..da7add25cc9 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -17,7 +17,7 @@ android { defaultConfig { applicationId = "io.sentry.uitest.android.critical" - minSdk = Config.Android.minSdkVersionCompose + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 1 versionName = "1.0" diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index 02609ba7875..e3afc9823ff 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -14,7 +14,7 @@ android { namespace = "io.sentry.uitest.android" defaultConfig { - minSdk = Config.Android.minSdkVersionCompose + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 1 versionName = "1.0.0" 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 index bfac0c3b5f2..f1ed40eeff7 100644 --- 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 @@ -9,8 +9,11 @@ import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnitRunner import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration import io.sentry.android.core.SentryAndroid import io.sentry.android.core.SentryAndroidOptions +import io.sentry.test.applyTestOptions +import io.sentry.test.initForTest import io.sentry.uitest.android.mockservers.MockRelay import java.io.FileInputStream import java.util.concurrent.TimeUnit @@ -84,7 +87,7 @@ abstract class BaseUiTest { if (relayWaitForRequests) { IdlingRegistry.getInstance().register(relayIdlingResource) } - SentryAndroid.init(context) { + initForTest(context) { it.dsn = mockDsn it.isDebug = true // We don't use test orchestrator, due to problems with Saucelabs. @@ -111,3 +114,10 @@ fun classExists(className: String): Boolean { } return false } + +fun initForTest(context: Context, optionsConfiguration: OptionsConfiguration) { + SentryAndroid.init(context) { + applyTestOptions(it) + optionsConfiguration.configure(it) + } +} diff --git a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt index cb6c772bb95..e9c08f43591 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt +++ b/sentry-android-integration-tests/sentry-uitest-android/src/androidTest/java/io/sentry/uitest/android/SdkInitTests.kt @@ -61,11 +61,25 @@ class SdkInitTests : BaseUiTest() { it.isDebug = true } relayIdlingResource.increment() + relayIdlingResource.increment() transaction.finish() sampleScenario.moveToState(Lifecycle.State.DESTROYED) val transaction2 = Sentry.startTransaction("e2etests2", "testInit") transaction2.finish() + relay.assert { + findEnvelope { + assertEnvelopeTransaction( + it.items.toList(), + AndroidLogger() + ).transaction == "e2etests" + }.assert { + val transactionItem: SentryTransaction = it.assertTransaction() + it.assertNoOtherItems() + assertEquals("e2etests", transactionItem.transaction) + } + } + relay.assert { findEnvelope { assertEnvelopeTransaction( 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 index e9569780866..f685e848748 100644 --- 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 @@ -93,7 +93,7 @@ class RelayAsserter( /** Request parsed as envelope. */ val envelope: SentryEnvelope? by lazy { try { - EnvelopeReader(Sentry.getCurrentHub().options.serializer) + EnvelopeReader(Sentry.getCurrentScopes().options.serializer) .read(GZIPInputStream(request.body.inputStream())) } catch (e: IOException) { null diff --git a/sentry-android-integration-tests/test-app-plain/build.gradle.kts b/sentry-android-integration-tests/test-app-plain/build.gradle.kts index 762bd2ce7ca..7a094fe2837 100644 --- a/sentry-android-integration-tests/test-app-plain/build.gradle.kts +++ b/sentry-android-integration-tests/test-app-plain/build.gradle.kts @@ -8,7 +8,7 @@ android { defaultConfig { applicationId = "io.sentry.java.tests.perf.appplain" - minSdk = Config.Android.minSdkVersionNdk + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 1 versionName = "1.0" diff --git a/sentry-android-integration-tests/test-app-sentry/build.gradle.kts b/sentry-android-integration-tests/test-app-sentry/build.gradle.kts index 3c067fb7ed7..6464ca9d570 100644 --- a/sentry-android-integration-tests/test-app-sentry/build.gradle.kts +++ b/sentry-android-integration-tests/test-app-sentry/build.gradle.kts @@ -8,7 +8,7 @@ android { defaultConfig { applicationId = "io.sentry.java.tests.perf.appsentry" - minSdk = Config.Android.minSdkVersionNdk + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 1 versionName = "1.0" diff --git a/sentry-android-navigation/api/sentry-android-navigation.api b/sentry-android-navigation/api/sentry-android-navigation.api index 79151bb3fb4..03a46d8b87b 100644 --- a/sentry-android-navigation/api/sentry-android-navigation.api +++ b/sentry-android-navigation/api/sentry-android-navigation.api @@ -10,11 +10,11 @@ public final class io/sentry/android/navigation/SentryNavigationListener : andro public static final field Companion Lio/sentry/android/navigation/SentryNavigationListener$Companion; public static final field NAVIGATION_OP Ljava/lang/String; public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Z)V - public fun (Lio/sentry/IHub;ZZ)V - public fun (Lio/sentry/IHub;ZZLjava/lang/String;)V - public synthetic fun (Lio/sentry/IHub;ZZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Z)V + public fun (Lio/sentry/IScopes;ZZ)V + public fun (Lio/sentry/IScopes;ZZLjava/lang/String;)V + public synthetic fun (Lio/sentry/IScopes;ZZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun onDestinationChanged (Landroidx/navigation/NavController;Landroidx/navigation/NavDestination;Landroid/os/Bundle;)V } diff --git a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt index 3d020f5a562..1ea9e42c3c9 100644 --- a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt +++ b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt @@ -7,9 +7,9 @@ import androidx.navigation.NavController import androidx.navigation.NavDestination import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ITransaction +import io.sentry.ScopesAdapter import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO @@ -35,7 +35,7 @@ private const val TRACE_ORIGIN = "auto.navigation" * with [SentryOptions.idleTimeout] for navigation events. */ class SentryNavigationListener @JvmOverloads constructor( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), private val enableNavigationBreadcrumbs: Boolean = true, private val enableNavigationTracing: Boolean = true, private val traceOriginAppendix: String? = null @@ -44,7 +44,7 @@ class SentryNavigationListener @JvmOverloads constructor( private var previousDestinationRef: WeakReference? = null private var previousArgs: Bundle? = null - private val isPerformanceEnabled get() = hub.options.isTracingEnabled && enableNavigationTracing + private val isPerformanceEnabled get() = scopes.options.isTracingEnabled && enableNavigationTracing private var activeTransaction: ITransaction? = null @@ -64,8 +64,8 @@ class SentryNavigationListener @JvmOverloads constructor( val routeName = destination.extractName(controller.context) if (routeName != null) { - if (hub.options.isEnableScreenTracking) { - hub.configureScope { it.screen = routeName } + if (scopes.options.isEnableScreenTracking) { + scopes.configureScope { it.screen = routeName } } startTracing(routeName, destination, toArguments) } @@ -98,7 +98,7 @@ class SentryNavigationListener @JvmOverloads constructor( } val hint = Hint() hint.set(TypeCheckHint.ANDROID_NAV_DESTINATION, destination) - hub.addBreadcrumb(breadcrumb, hint) + scopes.addBreadcrumb(breadcrumb, hint) } private fun startTracing( @@ -107,7 +107,7 @@ class SentryNavigationListener @JvmOverloads constructor( arguments: Map ) { if (!isPerformanceEnabled) { - TracingUtils.startNewTrace(hub) + TracingUtils.startNewTrace(scopes) return } @@ -118,7 +118,7 @@ class SentryNavigationListener @JvmOverloads constructor( if (destination.navigatorName == "activity") { // we do not trace navigation between activities to avoid clashing with activity lifecycle tracing - hub.options.logger.log( + scopes.options.logger.log( DEBUG, "Navigating to activity destination, no transaction captured." ) @@ -127,12 +127,12 @@ class SentryNavigationListener @JvmOverloads constructor( val transactionOptions = TransactionOptions().also { it.isWaitForChildren = true - it.idleTimeout = hub.options.idleTimeout + it.idleTimeout = scopes.options.idleTimeout it.deadlineTimeout = TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION it.isTrimEnd = true } - val transaction = hub.startTransaction( + val transaction = scopes.startTransaction( TransactionContext(routeName, TransactionNameSource.ROUTE, NAVIGATION_OP), transactionOptions ) @@ -144,7 +144,7 @@ class SentryNavigationListener @JvmOverloads constructor( if (arguments.isNotEmpty()) { transaction.setData("arguments", arguments) } - hub.configureScope { scope -> + scopes.configureScope { scope -> scope.withTransaction { tx -> if (tx == null) { scope.transaction = transaction @@ -159,7 +159,7 @@ class SentryNavigationListener @JvmOverloads constructor( activeTransaction?.finish(status) // clear transaction from scope so others can bind to it - hub.configureScope { scope -> + scopes.configureScope { scope -> scope.withTransaction { tx -> if (tx == activeTransaction) { scope.clearTransaction() @@ -182,7 +182,7 @@ class SentryNavigationListener @JvmOverloads constructor( val name = route ?: try { context.resources.getResourceEntryName(id) } catch (e: NotFoundException) { - hub.options.logger.log( + scopes.options.logger.log( DEBUG, "Destination id cannot be retrieved from Resources, no transaction captured." ) diff --git a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt index 342673dafba..e80cc299bdb 100644 --- a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt +++ b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt @@ -7,8 +7,8 @@ import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.Scope import io.sentry.Scope.IWithTransaction import io.sentry.ScopeCallback @@ -39,7 +39,7 @@ import kotlin.test.assertNull class SentryNavigationListenerTest { class Fixture { - val hub = mock() + val scopes = mock() val destination = mock() val navController = mock() @@ -55,7 +55,7 @@ class SentryNavigationListenerTest { toRoute: String? = "route", toId: String? = "destination-id-1", enableBreadcrumbs: Boolean = true, - enableTracing: Boolean = true, + enableNavigationTracing: Boolean = true, enableScreenTracking: Boolean = true, tracesSampleRate: Double? = 1.0, hasViewIdInRes: Boolean = true, @@ -69,20 +69,20 @@ class SentryNavigationListenerTest { ) isEnableScreenTracking = enableScreenTracking } - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) this.transaction = transaction ?: SentryTracer( TransactionContext( "/$toRoute", SentryNavigationListener.NAVIGATION_OP ), - hub + scopes ) - whenever(hub.startTransaction(any(), any())) + whenever(scopes.startTransaction(any(), any())) .thenReturn(this.transaction) - whenever(hub.configureScope(any())).thenAnswer { + whenever(scopes.configureScope(any())).thenAnswer { (it.arguments[0] as ScopeCallback).run(scope) } @@ -98,9 +98,9 @@ class SentryNavigationListenerTest { whenever(navController.context).thenReturn(context) whenever(destination.route).thenReturn(toRoute) return SentryNavigationListener( - hub, + scopes, enableBreadcrumbs, - enableTracing, + enableNavigationTracing, traceOriginAppendix ) } @@ -114,7 +114,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("navigation", it.type) assertEquals("navigation", it.category) @@ -135,7 +135,7 @@ class SentryNavigationListenerTest { bundleOf("arg1" to "foo", "arg2" to "bar") ) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("/route", it.data["to"]) assertEquals(mapOf("arg1" to "foo", "arg2" to "bar"), it.data["to_arguments"]) @@ -154,7 +154,7 @@ class SentryNavigationListenerTest { bundleOf() ) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("/route", it.data["to"]) assertNull(it.data["to_arguments"]) @@ -182,7 +182,7 @@ class SentryNavigationListenerTest { bundleOf("to_arg1" to "to_foo") ) val captor = argumentCaptor() - verify(fixture.hub, times(2)).addBreadcrumb(captor.capture(), any()) + verify(fixture.scopes, times(2)).addBreadcrumb(captor.capture(), any()) captor.secondValue.let { assertEquals("/route_from", it.data["from"]) assertEquals(mapOf("from_arg1" to "from_foo"), it.data["from_arguments"]) @@ -198,16 +198,16 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test fun `onDestinationChanged does not start tracing when tracing is disabled`() { - val sut = fixture.getSut(enableTracing = false) + val sut = fixture.getSut(enableNavigationTracing = false) sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -215,11 +215,11 @@ class SentryNavigationListenerTest { @Test fun `onDestinationChanged does not start tracing when tracesSampleRate is not set`() { - val sut = fixture.getSut(enableTracing = true, tracesSampleRate = null) + val sut = fixture.getSut(enableNavigationTracing = true, tracesSampleRate = null) sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -232,7 +232,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -244,7 +244,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -256,7 +256,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("/route", it.name) assertEquals(SentryNavigationListener.NAVIGATION_OP, it.operation) @@ -272,7 +272,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("/github", it.name) assertEquals(TransactionNameSource.ROUTE, it.transactionNameSource) @@ -287,7 +287,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("/destination-id-1", it.name) assertEquals(TransactionNameSource.ROUTE, it.transactionNameSource) @@ -306,7 +306,7 @@ class SentryNavigationListenerTest { bundleOf("user_id" to 123, "per_page" to 10) ) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("/github", it.name) assertEquals(TransactionNameSource.ROUTE, it.transactionNameSource) @@ -361,19 +361,19 @@ class SentryNavigationListenerTest { @Test fun `starts new trace if performance is disabled`() { - val sut = fixture.getSut(enableTracing = false) + val sut = fixture.getSut(enableNavigationTracing = false) val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = Scope(fixture.options) val propagationContextAtStart = scope.propagationContext - whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { + whenever(fixture.scopes.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub, times(2)).configureScope(any()) + verify(fixture.scopes, times(2)).configureScope(any()) assertNotSame(propagationContextAtStart, scope.propagationContext) } @@ -401,7 +401,7 @@ class SentryNavigationListenerTest { sut.onDestinationChanged(fixture.navController, fixture.destination, null) - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( any(), check { options -> assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, options.deadlineTimeout) diff --git a/sentry-android-ndk/CMakeLists.txt b/sentry-android-ndk/CMakeLists.txt deleted file mode 100644 index ff5fc2540b5..00000000000 --- a/sentry-android-ndk/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(Sentry-Android LANGUAGES C CXX) - -# Add sentry-android shared library -add_library(sentry-android SHARED src/main/jni/sentry.c) - -# make sure that we build it as a shared lib instead of a static lib -set(BUILD_SHARED_LIBS ON) -set(SENTRY_BUILD_SHARED_LIBS ON) - -# Adding sentry-native submodule subdirectory -add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) - -# Link to sentry-native -target_link_libraries(sentry-android PRIVATE - $ -) - -# Android 15: Support 16KB page sizes -# see https://developer.android.com/guide/practices/page-sizes -target_link_options(sentry PRIVATE "-Wl,-z,max-page-size=16384") -target_link_options(sentry-android PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index e8f838ce8b4..155a368b11e 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -7,7 +7,7 @@ public final class io/sentry/android/ndk/BuildConfig { } public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/core/IDebugImagesLoader { - public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/ndk/NativeModuleListLoader;)V + public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V public fun clearDebugImages ()V public fun loadDebugImages ()Ljava/util/List; } diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index ee0819eb83f..339c1682146 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -5,38 +5,19 @@ plugins { kotlin("android") jacoco id(Config.QualityPlugins.jacocoAndroid) - id(Config.NativePlugins.nativeBundleExport) id(Config.QualityPlugins.gradleVersions) } -var sentryNativeSrc: String = "sentry-native" -val sentryAndroidSdkName: String by project - android { compileSdk = Config.Android.compileSdkVersion namespace = "io.sentry.android.ndk" - sentryNativeSrc = if (File("${project.projectDir}/sentry-native-local").exists()) { - "sentry-native-local" - } else { - "sentry-native" - } - println("sentry-android-ndk: $sentryNativeSrc") - defaultConfig { targetSdk = Config.Android.targetSdkVersion - minSdk = Config.Android.minSdkVersionNdk // NDK requires a higher API level than core. + minSdk = Config.Android.minSdkVersion 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") - } - } - ndk { abiFilters.addAll(Config.Android.abiFilters) } @@ -45,15 +26,6 @@ android { buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") } - // we use the default NDK and CMake versions based on the AGP's version - // https://developer.android.com/studio/projects/install-ndk#apply-specific-version - - externalNativeBuild { - cmake { - path("CMakeLists.txt") - } - } - buildTypes { getByName("debug") getByName("release") { @@ -81,10 +53,6 @@ android { checkReleaseBuilds = false } - nativeBundleExport { - headerDir = "${project.projectDir}/$sentryNativeSrc/include" - } - // needed because of Kotlin 1.4.x configurations.all { resolutionStrategy.force(Config.CompileOnly.jetbrainsAnnotations) @@ -108,6 +76,8 @@ dependencies { api(projects.sentry) api(projects.sentryAndroidCore) + implementation(Config.Libs.sentryNativeNdk) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) testImplementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) diff --git a/sentry-android-ndk/sentry-native b/sentry-android-ndk/sentry-native deleted file mode 160000 index 4072538dfdb..00000000000 --- a/sentry-android-ndk/sentry-native +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4072538dfdbcafb3974318fe08798e51f62786d9 diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java index cb38db498a6..1257325091a 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/DebugImagesLoader.java @@ -1,12 +1,15 @@ package io.sentry.android.ndk; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.IDebugImagesLoader; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.ndk.NativeModuleListLoader; import io.sentry.protocol.DebugImage; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,7 +28,8 @@ public final class DebugImagesLoader implements IDebugImagesLoader { private static @Nullable List debugImages; /** we need to lock it because it could be called from different threads */ - private static final @NotNull Object debugImagesLock = new Object(); + protected static final @NotNull AutoClosableReentrantLock debugImagesLock = + new AutoClosableReentrantLock(); public DebugImagesLoader( final @NotNull SentryAndroidOptions options, @@ -42,12 +46,23 @@ public DebugImagesLoader( */ @Override public @Nullable List loadDebugImages() { - synchronized (debugImagesLock) { + try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) { if (debugImages == null) { try { - final DebugImage[] debugImagesArr = moduleListLoader.loadModuleList(); + final io.sentry.ndk.DebugImage[] debugImagesArr = moduleListLoader.loadModuleList(); if (debugImagesArr != null) { - debugImages = Arrays.asList(debugImagesArr); + debugImages = new ArrayList<>(debugImagesArr.length); + for (io.sentry.ndk.DebugImage d : debugImagesArr) { + final DebugImage debugImage = new DebugImage(); + debugImage.setUuid(d.getUuid()); + debugImage.setType(d.getType()); + debugImage.setDebugId(d.getDebugId()); + debugImage.setCodeId(d.getCodeId()); + debugImage.setImageAddr(d.getImageAddr()); + debugImage.setImageSize(d.getImageSize()); + debugImage.setArch(d.getArch()); + debugImages.add(debugImage); + } options .getLogger() .log(SentryLevel.DEBUG, "Debug images loaded: %d", debugImages.size()); @@ -63,7 +78,7 @@ public DebugImagesLoader( /** Clears the caching of debug images on sentry-native and here. */ @Override public void clearDebugImages() { - synchronized (debugImagesLock) { + try (final @NotNull ISentryLifecycleToken ignored = debugImagesLock.acquire()) { try { moduleListLoader.clearModuleList(); diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java deleted file mode 100644 index a8d50e40fe0..00000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/INativeScope.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.sentry.android.ndk; - -interface INativeScope { - void setTag(String key, String value); - - void removeTag(String key); - - void setExtra(String key, String value); - - void removeExtra(String key); - - void setUser(String id, String email, String ipAddress, String username); - - void removeUser(); - - void addBreadcrumb( - String level, String message, String category, String type, String timestamp, String data); -} diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java deleted file mode 100644 index 464fcd3992e..00000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeModuleListLoader.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.sentry.android.ndk; - -import io.sentry.protocol.DebugImage; -import org.jetbrains.annotations.Nullable; - -final class NativeModuleListLoader { - - public @Nullable DebugImage[] loadModuleList() { - return nativeLoadModuleList(); - } - - public void clearModuleList() { - nativeClearModuleList(); - } - - public static native DebugImage[] nativeLoadModuleList(); - - public static native void nativeClearModuleList(); -} diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java deleted file mode 100644 index 9d82f9d5c80..00000000000 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NativeScope.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.sentry.android.ndk; - -final class NativeScope implements INativeScope { - @Override - public void setTag(String key, String value) { - nativeSetTag(key, value); - } - - @Override - public void removeTag(String key) { - nativeRemoveTag(key); - } - - @Override - public void setExtra(String key, String value) { - nativeSetExtra(key, value); - } - - @Override - public void removeExtra(String key) { - nativeRemoveExtra(key); - } - - @Override - public void setUser(String id, String email, String ipAddress, String username) { - nativeSetUser(id, email, ipAddress, username); - } - - @Override - public void removeUser() { - nativeRemoveUser(); - } - - @Override - public void addBreadcrumb( - String level, String message, String category, String type, String timestamp, String data) { - nativeAddBreadcrumb(level, message, category, type, timestamp, data); - } - - public static native void nativeSetTag(String key, String value); - - public static native void nativeRemoveTag(String key); - - public static native void nativeSetExtra(String key, String value); - - public static native void nativeRemoveExtra(String key); - - public static native void nativeSetUser( - String id, String email, String ipAddress, String username); - - public static native void nativeRemoveUser(); - - public static native void nativeAddBreadcrumb( - String level, String message, String category, String type, String timestamp, String data); -} diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java index 2350c419bea..118b1f68511 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java @@ -5,6 +5,8 @@ import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.ndk.INativeScope; +import io.sentry.ndk.NativeScope; import io.sentry.protocol.User; import io.sentry.util.Objects; import java.util.Locale; 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 3429780eecc..a37475002ef 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 @@ -1,6 +1,9 @@ package io.sentry.android.ndk; import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.ndk.NativeModuleListLoader; +import io.sentry.ndk.NdkOptions; +import io.sentry.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.ApiStatus; @@ -16,19 +19,9 @@ private SentryNdk() {} static { new Thread( () -> { - // On older Android versions, it was necessary to manually call "`System.loadLibrary` - // on all - // transitive dependencies before loading [the] main library." - // The dependencies of `libsentry.so` are currently `lib{c,m,dl,log}.so`. - // See - // https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#changes-to-library-dependency-resolution try { - System.loadLibrary("log"); - System.loadLibrary("sentry"); - System.loadLibrary("sentry-android"); - } catch (Throwable t) { - // ignored - // if loadLibrary() fails, the later init() will throw an exception anyway + //noinspection UnstableApiUsage + io.sentry.ndk.SentryNdk.loadNativeLibraries(); } finally { loadLibraryLatch.countDown(); } @@ -37,10 +30,6 @@ private SentryNdk() {} .start(); } - private static native void initSentryNative(@NotNull final SentryAndroidOptions options); - - private static native void shutdown(); - /** * Init the NDK integration * @@ -50,7 +39,20 @@ public static void init(@NotNull final SentryAndroidOptions options) { SentryNdkUtil.addPackage(options.getSdkVersion()); try { if (loadLibraryLatch.await(2000, TimeUnit.MILLISECONDS)) { - initSentryNative(options); + final @NotNull NdkOptions ndkOptions = + new NdkOptions( + Objects.requireNonNull(options.getDsn(), "DSN is required for sentry-ndk"), + options.isDebug(), + Objects.requireNonNull( + options.getOutboxPath(), "outbox path is required for sentry-ndk"), + options.getRelease(), + options.getEnvironment(), + options.getDist(), + options.getMaxBreadcrumbs(), + options.getNativeSdkName()); + + //noinspection UnstableApiUsage + io.sentry.ndk.SentryNdk.init(ndkOptions); // only add scope sync observer if the scope sync is enabled. if (options.isEnableScopeSync()) { @@ -71,7 +73,8 @@ public static void init(@NotNull final SentryAndroidOptions options) { public static void close() { try { if (loadLibraryLatch.await(2000, TimeUnit.MILLISECONDS)) { - shutdown(); + //noinspection UnstableApiUsage + io.sentry.ndk.SentryNdk.close(); } else { throw new IllegalStateException("Timeout waiting for Sentry NDK library to load"); } diff --git a/sentry-android-ndk/src/main/jni/sentry.c b/sentry-android-ndk/src/main/jni/sentry.c deleted file mode 100644 index b92e8495239..00000000000 --- a/sentry-android-ndk/src/main/jni/sentry.c +++ /dev/null @@ -1,499 +0,0 @@ -#include -#include -#include -#include -#include - -#define ENSURE(Expr) \ - if (!(Expr)) \ - return - -#define ENSURE_OR_FAIL(Expr) \ - if (!(Expr)) \ - goto fail - -static bool get_string_into(JNIEnv *env, jstring jstr, char* buf, size_t buf_len) -{ - jsize utf_len = (*env)->GetStringUTFLength(env, jstr); - if ((size_t)utf_len >= buf_len) { - return false; - } - - jsize j_len = (*env)->GetStringLength(env, jstr); - - (*env)->GetStringUTFRegion(env, jstr, 0, j_len, buf); - if ((*env)->ExceptionCheck(env) == JNI_TRUE) { - return false; - } - - buf[utf_len] = '\0'; - return true; -} - -static char* get_string(JNIEnv *env, jstring jstr) { - char *buf = NULL; - - jsize utf_len = (*env)->GetStringUTFLength(env, jstr); - size_t buf_len = (size_t)utf_len + 1; - buf = sentry_malloc(buf_len); - ENSURE_OR_FAIL(buf); - - ENSURE_OR_FAIL(get_string_into(env, jstr, buf, buf_len)); - - return buf; - -fail: - sentry_free(buf); - - return NULL; -} - -static char *call_get_string(JNIEnv *env, jobject obj, jmethodID mid) -{ - jstring j_str = (jstring)(*env)->CallObjectMethod(env, obj, mid); - ENSURE_OR_FAIL(j_str); - char* str = get_string(env, j_str); - (*env)->DeleteLocalRef(env, j_str); - - return str; - -fail: - return NULL; -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetTag( - JNIEnv *env, - jclass cls, - jstring key, - jstring value) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - const char *charValue = (*env)->GetStringUTFChars(env, value, 0); - - sentry_set_tag(charKey, charValue); - - (*env)->ReleaseStringUTFChars(env, key, charKey); - (*env)->ReleaseStringUTFChars(env, value, charValue); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveTag(JNIEnv *env, jclass cls, jstring key) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - - sentry_remove_tag(charKey); - - (*env)->ReleaseStringUTFChars(env, key, charKey); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetExtra( - JNIEnv *env, - jclass cls, - jstring key, - jstring value) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - const char *charValue = (*env)->GetStringUTFChars(env, value, 0); - - sentry_value_t sentryValue = sentry_value_new_string(charValue); - sentry_set_extra(charKey, sentryValue); - - (*env)->ReleaseStringUTFChars(env, key, charKey); - (*env)->ReleaseStringUTFChars(env, value, charValue); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveExtra(JNIEnv *env, jclass cls, jstring key) { - const char *charKey = (*env)->GetStringUTFChars(env, key, 0); - - sentry_remove_extra(charKey); - - (*env)->ReleaseStringUTFChars(env, key, charKey); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeSetUser( - JNIEnv *env, - jclass cls, - jstring id, - jstring email, - jstring ipAddress, - jstring username) { - sentry_value_t user = sentry_value_new_object(); - if (id) { - const char *charId = (*env)->GetStringUTFChars(env, id, 0); - sentry_value_set_by_key(user, "id", sentry_value_new_string(charId)); - (*env)->ReleaseStringUTFChars(env, id, charId); - } - if (email) { - const char *charEmail = (*env)->GetStringUTFChars(env, email, 0); - sentry_value_set_by_key( - user, "email", sentry_value_new_string(charEmail)); - (*env)->ReleaseStringUTFChars(env, email, charEmail); - } - if (ipAddress) { - const char *charIpAddress = (*env)->GetStringUTFChars(env, ipAddress, 0); - sentry_value_set_by_key( - user, "ip_address", sentry_value_new_string(charIpAddress)); - (*env)->ReleaseStringUTFChars(env, ipAddress, charIpAddress); - } - if (username) { - const char *charUsername = (*env)->GetStringUTFChars(env, username, 0); - sentry_value_set_by_key( - user, "username", sentry_value_new_string(charUsername)); - (*env)->ReleaseStringUTFChars(env, username, charUsername); - } - sentry_set_user(user); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeRemoveUser(JNIEnv *env, jclass cls) { - sentry_remove_user(); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeScope_nativeAddBreadcrumb( - JNIEnv *env, - jclass cls, - jstring level, - jstring message, - jstring category, - jstring type, - jstring timestamp, - jstring data) { - if (!level && !message && !category && !type) { - return; - } - const char *charMessage = NULL; - if (message) { - charMessage = (*env)->GetStringUTFChars(env, message, 0); - } - const char *charType = NULL; - if (type) { - charType = (*env)->GetStringUTFChars(env, type, 0); - } - sentry_value_t crumb = sentry_value_new_breadcrumb(charType, charMessage); - - if (charMessage) { - (*env)->ReleaseStringUTFChars(env, message, charMessage); - } - if (charType) { - (*env)->ReleaseStringUTFChars(env, type, charType); - } - - if (category) { - const char *charCategory = (*env)->GetStringUTFChars(env, category, 0); - sentry_value_set_by_key( - crumb, "category", sentry_value_new_string(charCategory)); - (*env)->ReleaseStringUTFChars(env, category, charCategory); - } - if (level) { - const char *charLevel = (*env)->GetStringUTFChars(env, level, 0); - sentry_value_set_by_key( - crumb, "level", sentry_value_new_string(charLevel)); - (*env)->ReleaseStringUTFChars(env, level, charLevel); - } - - if (timestamp) { - // overwrite timestamp that is already created on sentry_value_new_breadcrumb - const char *charTimestamp = (*env)->GetStringUTFChars(env, timestamp, 0); - sentry_value_set_by_key( - crumb, "timestamp", sentry_value_new_string(charTimestamp)); - (*env)->ReleaseStringUTFChars(env, timestamp, charTimestamp); - } - - if (data) { - const char *charData = (*env)->GetStringUTFChars(env, data, 0); - - // we create an object because the Java layer parses it as a Map - sentry_value_t dataObject = sentry_value_new_object(); - sentry_value_set_by_key(dataObject, "data", sentry_value_new_string(charData)); - - sentry_value_set_by_key(crumb, "data", dataObject); - - (*env)->ReleaseStringUTFChars(env, data, charData); - } - - sentry_add_breadcrumb(crumb); -} - -static void send_envelope(sentry_envelope_t *envelope, void *data) { - const char *outbox_path = (const char *) data; - char envelope_id_str[40]; - - sentry_uuid_t envelope_id = sentry_uuid_new_v4(); - sentry_uuid_as_string(&envelope_id, envelope_id_str); - - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 42; // "/" + envelope_id_str + "\0" = 42 - char* envelope_path = sentry_malloc(final_len); - ENSURE(envelope_path); - int written = snprintf(envelope_path, final_len, "%s/%s", outbox_path, envelope_id_str); - if (written > outbox_len && written < final_len) { - sentry_envelope_write_to_file(envelope, envelope_path); - } - - sentry_free(envelope_path); - sentry_envelope_free(envelope); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_SentryNdk_initSentryNative( - JNIEnv *env, - jclass cls, - jobject sentry_sdk_options) { - jclass options_cls = (*env)->GetObjectClass(env, sentry_sdk_options); - jmethodID outbox_path_mid = (*env)->GetMethodID(env, options_cls, "getOutboxPath", - "()Ljava/lang/String;"); - jmethodID dsn_mid = (*env)->GetMethodID(env, options_cls, "getDsn", "()Ljava/lang/String;"); - jmethodID is_debug_mid = (*env)->GetMethodID(env, options_cls, "isDebug", "()Z"); - jmethodID release_mid = (*env)->GetMethodID(env, options_cls, "getRelease", - "()Ljava/lang/String;"); - jmethodID environment_mid = (*env)->GetMethodID(env, options_cls, "getEnvironment", - "()Ljava/lang/String;"); - jmethodID dist_mid = (*env)->GetMethodID(env, options_cls, "getDist", "()Ljava/lang/String;"); - jmethodID max_crumbs_mid = (*env)->GetMethodID(env, options_cls, "getMaxBreadcrumbs", "()I"); - jmethodID native_sdk_name_mid = (*env)->GetMethodID(env, options_cls, "getNativeSdkName", - "()Ljava/lang/String;"); - - jmethodID handler_strategy_mid = (*env)->GetMethodID(env, options_cls, "getNdkHandlerStrategy", "()I"); - - (*env)->DeleteLocalRef(env, options_cls); - - char *outbox_path = NULL; - sentry_transport_t *transport = NULL; - bool transport_owns_path = false; - sentry_options_t *options = NULL; - bool options_owns_transport = false; - char *dsn_str = NULL; - char *release_str = NULL; - char *environment_str = NULL; - char *dist_str = NULL; - char *native_sdk_name_str = NULL; - - options = sentry_options_new(); - ENSURE_OR_FAIL(options); - - // session tracking is enabled by default, but the Android SDK already handles it - sentry_options_set_auto_session_tracking(options, 0); - - jboolean debug = (jboolean)(*env)->CallBooleanMethod(env, sentry_sdk_options, is_debug_mid); - sentry_options_set_debug(options, debug); - - jint max_crumbs = (jint) (*env)->CallIntMethod(env, sentry_sdk_options, max_crumbs_mid); - sentry_options_set_max_breadcrumbs(options, max_crumbs); - - outbox_path = call_get_string(env, sentry_sdk_options, outbox_path_mid); - ENSURE_OR_FAIL(outbox_path); - - transport = sentry_transport_new(send_envelope); - ENSURE_OR_FAIL(transport); - sentry_transport_set_state(transport, outbox_path); - sentry_transport_set_free_func(transport, sentry_free); - transport_owns_path = true; - - sentry_options_set_transport(options, transport); - options_owns_transport = true; - - // give sentry-native its own database path it can work with, next to the outbox - size_t outbox_len = strlen(outbox_path); - size_t final_len = outbox_len + 15; // len(".sentry-native\0") = 15 - char* database_path = sentry_malloc(final_len); - ENSURE_OR_FAIL(database_path); - strncpy(database_path, outbox_path, final_len); - char *dir = strrchr(database_path, '/'); - if (dir) - { - strncpy(dir + 1, ".sentry-native", final_len - (dir + 1 - database_path)); - } - sentry_options_set_database_path(options, database_path); - sentry_free(database_path); - - dsn_str = call_get_string(env, sentry_sdk_options, dsn_mid); - ENSURE_OR_FAIL(dsn_str); - sentry_options_set_dsn(options, dsn_str); - sentry_free(dsn_str); - - release_str = call_get_string(env, sentry_sdk_options, release_mid); - if (release_str) { - sentry_options_set_release(options, release_str); - sentry_free(release_str); - } - - environment_str = call_get_string(env, sentry_sdk_options, environment_mid); - if (environment_str) - { - sentry_options_set_environment(options, environment_str); - sentry_free(environment_str); - } - - dist_str = call_get_string(env, sentry_sdk_options, dist_mid); - if (dist_str) - { - sentry_options_set_dist(options, dist_str); - sentry_free(dist_str); - } - - native_sdk_name_str = call_get_string(env, sentry_sdk_options, native_sdk_name_mid); - if (native_sdk_name_str) { - sentry_options_set_sdk_name(options, native_sdk_name_str); - sentry_free(native_sdk_name_str); - } - - jint handler_strategy = (jint) (*env)->CallIntMethod(env, sentry_sdk_options, handler_strategy_mid); - sentry_options_set_handler_strategy(options, handler_strategy); - - sentry_init(options); - return; - -fail: - if (!transport_owns_path) { - sentry_free(outbox_path); - } - if (!options_owns_transport) { - sentry_transport_free(transport); - } - sentry_options_free(options); -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_NativeModuleListLoader_nativeClearModuleList(JNIEnv *env, jclass cls) { - sentry_clear_modulecache(); -} - -JNIEXPORT jobjectArray JNICALL -Java_io_sentry_android_ndk_NativeModuleListLoader_nativeLoadModuleList(JNIEnv *env, jclass cls) { - sentry_value_t image_list_t = sentry_get_modules_list(); - jobjectArray image_list = NULL; - - if (sentry_value_get_type(image_list_t) == SENTRY_VALUE_TYPE_LIST) { - size_t len_t = sentry_value_get_length(image_list_t); - - jclass image_class = (*env)->FindClass(env, "io/sentry/protocol/DebugImage"); - image_list = (*env)->NewObjectArray(env, len_t, image_class, NULL); - - jmethodID image_addr_method = (*env)->GetMethodID(env, image_class, "setImageAddr", - "(Ljava/lang/String;)V"); - - jmethodID image_size_method = (*env)->GetMethodID(env, image_class, "setImageSize", - "(J)V"); - - jmethodID code_file_method = (*env)->GetMethodID(env, image_class, "setCodeFile", - "(Ljava/lang/String;)V"); - - jmethodID image_addr_ctor = (*env)->GetMethodID(env, image_class, "", - "()V"); - - jmethodID type_method = (*env)->GetMethodID(env, image_class, "setType", - "(Ljava/lang/String;)V"); - - jmethodID debug_id_method = (*env)->GetMethodID(env, image_class, "setDebugId", - "(Ljava/lang/String;)V"); - - jmethodID code_id_method = (*env)->GetMethodID(env, image_class, "setCodeId", - "(Ljava/lang/String;)V"); - - jmethodID debug_file_method = (*env)->GetMethodID(env, image_class, "setDebugFile", - "(Ljava/lang/String;)V"); - - for (size_t i = 0; i < len_t; i++) { - sentry_value_t image_t = sentry_value_get_by_index(image_list_t, i); - - if (!sentry_value_is_null(image_t)) { - jobject image = (*env)->NewObject(env, image_class, image_addr_ctor); - - sentry_value_t image_addr_t = sentry_value_get_by_key(image_t, "image_addr"); - if (!sentry_value_is_null(image_addr_t)) { - - const char *value_v = sentry_value_as_string(image_addr_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, image_addr_method, value); - - // Local refs (eg NewStringUTF) are freed automatically when the native method - // returns, but if you're iterating a large array, it's recommended to release - // manually due to allocation limits (512) on Android < 8 or OOM. - // https://developer.android.com/training/articles/perf-jni.html#local-and-global-references - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t image_size_t = sentry_value_get_by_key(image_t, "image_size"); - if (!sentry_value_is_null(image_size_t)) { - - int32_t value_v = sentry_value_as_int32(image_size_t); - jlong value = (jlong) value_v; - - (*env)->CallVoidMethod(env, image, image_size_method, value); - } - - sentry_value_t code_file_t = sentry_value_get_by_key(image_t, "code_file"); - if (!sentry_value_is_null(code_file_t)) { - - const char *value_v = sentry_value_as_string(code_file_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, code_file_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t code_type_t = sentry_value_get_by_key(image_t, "type"); - if (!sentry_value_is_null(code_type_t)) { - - const char *value_v = sentry_value_as_string(code_type_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, type_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t debug_id_t = sentry_value_get_by_key(image_t, "debug_id"); - if (!sentry_value_is_null(code_type_t)) { - - const char *value_v = sentry_value_as_string(debug_id_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, debug_id_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - sentry_value_t code_id_t = sentry_value_get_by_key(image_t, "code_id"); - if (!sentry_value_is_null(code_id_t)) { - - const char *value_v = sentry_value_as_string(code_id_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, code_id_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - // not needed on Android, but keeping for forward compatibility - sentry_value_t debug_file_t = sentry_value_get_by_key(image_t, "debug_file"); - if (!sentry_value_is_null(debug_file_t)) { - - const char *value_v = sentry_value_as_string(debug_file_t); - jstring value = (*env)->NewStringUTF(env, value_v); - - (*env)->CallVoidMethod(env, image, debug_file_method, value); - - (*env)->DeleteLocalRef(env, value); - } - - (*env)->SetObjectArrayElement(env, image_list, i, image); - - (*env)->DeleteLocalRef(env, image); - } - } - - sentry_value_decref(image_list_t); - } - - return image_list; -} - -JNIEXPORT void JNICALL -Java_io_sentry_android_ndk_SentryNdk_shutdown(JNIEnv *env, jclass cls) { - sentry_close(); -} diff --git a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt index db584c814f6..927ce98c3bd 100644 --- a/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt +++ b/sentry-android-ndk/src/test/java/io/sentry/android/ndk/DebugImagesLoaderTest.kt @@ -1,11 +1,10 @@ package io.sentry.android.ndk import io.sentry.android.core.SentryAndroidOptions -import io.sentry.protocol.DebugImage +import io.sentry.ndk.NativeModuleListLoader import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.lang.RuntimeException import kotlin.test.Test import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -38,7 +37,7 @@ class DebugImagesLoaderTest { whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf()) assertNotNull(sut.loadDebugImages()) - whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(DebugImage())) + whenever(fixture.nativeLoader.loadModuleList()).thenReturn(arrayOf(io.sentry.ndk.DebugImage())) assertTrue(sut.loadDebugImages()!!.isEmpty()) } 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 110f794cf9b..07e0e2828b5 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,6 +5,7 @@ import io.sentry.DateUtils import io.sentry.JsonSerializer import io.sentry.SentryLevel import io.sentry.SentryOptions +import io.sentry.ndk.INativeScope import io.sentry.protocol.User import io.sentry.test.DeferredExecutorService import io.sentry.test.ImmediateExecutorService diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api deleted file mode 100644 index a1ad9114a28..00000000000 --- a/sentry-android-okhttp/api/sentry-android-okhttp.api +++ /dev/null @@ -1,62 +0,0 @@ -public final class io/sentry/android/okhttp/BuildConfig { - public static final field BUILD_TYPE Ljava/lang/String; - public static final field DEBUG Z - public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; - public fun ()V -} - -public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { - public fun ()V - public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;)V - public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/IHub;Lokhttp3/EventListener;)V - public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lokhttp3/EventListener$Factory;)V - public fun (Lokhttp3/EventListener;)V - public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V - public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V - public fun cacheMiss (Lokhttp3/Call;)V - public fun callEnd (Lokhttp3/Call;)V - public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V - public fun callStart (Lokhttp3/Call;)V - public fun canceled (Lokhttp3/Call;)V - public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V - public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V - public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V - public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V - public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V - public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V - public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V - public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V - public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V - public fun requestBodyEnd (Lokhttp3/Call;J)V - public fun requestBodyStart (Lokhttp3/Call;)V - public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V - public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V - public fun requestHeadersStart (Lokhttp3/Call;)V - public fun responseBodyEnd (Lokhttp3/Call;J)V - public fun responseBodyStart (Lokhttp3/Call;)V - public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V - public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V - public fun responseHeadersStart (Lokhttp3/Call;)V - public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V - public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V - public fun secureConnectStart (Lokhttp3/Call;)V -} - -public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { - public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V - public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; -} - -public abstract interface class io/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback { - public abstract fun execute (Lio/sentry/ISpan;Lokhttp3/Request;Lokhttp3/Response;)Lio/sentry/ISpan; -} - diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts deleted file mode 100644 index 67a4729e562..00000000000 --- a/sentry-android-okhttp/build.gradle.kts +++ /dev/null @@ -1,85 +0,0 @@ -import io.gitlab.arturbosch.detekt.Detekt -import org.jetbrains.kotlin.config.KotlinCompilerVersion - -plugins { - id("com.android.library") - kotlin("android") - jacoco - id(Config.QualityPlugins.jacocoAndroid) - id(Config.QualityPlugins.gradleVersions) - id(Config.QualityPlugins.detektPlugin) -} - -android { - compileSdk = Config.Android.compileSdkVersion - namespace = "io.sentry.android.okhttp" - - defaultConfig { - targetSdk = Config.Android.targetSdkVersion - minSdk = Config.Android.minSdkVersionOkHttp - - // for AGP 4.1 - buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") - } - - buildTypes { - getByName("debug") - getByName("release") { - consumerProguardFiles("proguard-rules.pro") - } - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion - } - - testOptions { - animationsDisabled = true - unitTests.apply { - isReturnDefaultValues = true - isIncludeAndroidResources = true - } - } - - 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 - } - } -} - -kotlin { - explicitApi() -} - -dependencies { - api(projects.sentry) - api(projects.sentryOkhttp) - - compileOnly(Config.Libs.okhttp) - - implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) - - // tests - testImplementation(projects.sentryTestSupport) - testImplementation(Config.Libs.okhttp) - testImplementation(Config.TestLibs.kotlinTestJunit) - testImplementation(Config.TestLibs.androidxJunit) - testImplementation(Config.TestLibs.mockitoKotlin) - testImplementation(Config.TestLibs.mockitoInline) - testImplementation(Config.TestLibs.mockWebserver) -} - -tasks.withType { - // Target version of the generated JVM bytecode. It is used for type resolution. - jvmTarget = JavaVersion.VERSION_1_8.toString() -} diff --git a/sentry-android-okhttp/proguard-rules.pro b/sentry-android-okhttp/proguard-rules.pro deleted file mode 100644 index 9407448f6d6..00000000000 --- a/sentry-android-okhttp/proguard-rules.pro +++ /dev/null @@ -1,15 +0,0 @@ -##---------------Begin: proguard configuration for 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/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 ---------- -# We keep this name to avoid the sentry-okttp module to call the old listener multiple times --keepnames class io.sentry.android.okhttp.SentryOkHttpEventListener diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt deleted file mode 100644 index 7ca5313d8f6..00000000000 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt +++ /dev/null @@ -1,201 +0,0 @@ -package io.sentry.android.okhttp - -import io.sentry.HubAdapter -import io.sentry.IHub -import okhttp3.Call -import okhttp3.Connection -import okhttp3.EventListener -import okhttp3.Handshake -import okhttp3.HttpUrl -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Proxy - -/** - * Logs network performance event metrics to Sentry - * - * Usage - add instance of [SentryOkHttpEventListener] in [okhttp3.OkHttpClient.Builder.eventListener] - * - * ``` - * val client = OkHttpClient.Builder() - * .eventListener(SentryOkHttpEventListener()) - * .addInterceptor(SentryOkHttpInterceptor()) - * .build() - * ``` - * - * If you already use a [okhttp3.EventListener], you can pass it in the constructor. - * - * ``` - * val client = OkHttpClient.Builder() - * .eventListener(SentryOkHttpEventListener(myEventListener)) - * .addInterceptor(SentryOkHttpInterceptor()) - * .build() - * ``` - */ -@Deprecated( - "Use SentryOkHttpEventListener from sentry-okhttp instead", - ReplaceWith("SentryOkHttpEventListener", "io.sentry.okhttp.SentryOkHttpEventListener") -) -@Suppress("TooManyFunctions") -class SentryOkHttpEventListener( - hub: IHub = HubAdapter.getInstance(), - originalEventListenerCreator: ((call: Call) -> EventListener)? = null -) : EventListener() { - constructor() : this( - HubAdapter.getInstance(), - originalEventListenerCreator = null - ) - - constructor(originalEventListener: EventListener) : this( - HubAdapter.getInstance(), - originalEventListenerCreator = { originalEventListener } - ) - - constructor(originalEventListenerFactory: Factory) : this( - HubAdapter.getInstance(), - originalEventListenerCreator = { originalEventListenerFactory.create(it) } - ) - - constructor(hub: IHub = HubAdapter.getInstance(), originalEventListener: EventListener) : this( - hub, - originalEventListenerCreator = { originalEventListener } - ) - - constructor(hub: IHub = HubAdapter.getInstance(), originalEventListenerFactory: Factory) : this( - hub, - originalEventListenerCreator = { originalEventListenerFactory.create(it) } - ) - - private val delegate = io.sentry.okhttp.SentryOkHttpEventListener(hub, originalEventListenerCreator) - - override fun cacheConditionalHit(call: Call, cachedResponse: Response) { - delegate.cacheConditionalHit(call, cachedResponse) - } - - override fun cacheHit(call: Call, response: Response) { - delegate.cacheHit(call, response) - } - - override fun cacheMiss(call: Call) { - delegate.cacheMiss(call) - } - - override fun callEnd(call: Call) { - delegate.callEnd(call) - } - - override fun callFailed(call: Call, ioe: IOException) { - delegate.callFailed(call, ioe) - } - - override fun callStart(call: Call) { - delegate.callStart(call) - } - - override fun canceled(call: Call) { - delegate.canceled(call) - } - - override fun connectEnd( - call: Call, - inetSocketAddress: InetSocketAddress, - proxy: Proxy, - protocol: Protocol? - ) { - delegate.connectEnd(call, inetSocketAddress, proxy, protocol) - } - - override fun connectFailed( - call: Call, - inetSocketAddress: InetSocketAddress, - proxy: Proxy, - protocol: Protocol?, - ioe: IOException - ) { - delegate.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) - } - - override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { - delegate.connectStart(call, inetSocketAddress, proxy) - } - - override fun connectionAcquired(call: Call, connection: Connection) { - delegate.connectionAcquired(call, connection) - } - - override fun connectionReleased(call: Call, connection: Connection) { - delegate.connectionReleased(call, connection) - } - - override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { - delegate.dnsEnd(call, domainName, inetAddressList) - } - - override fun dnsStart(call: Call, domainName: String) { - delegate.dnsStart(call, domainName) - } - - override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { - delegate.proxySelectEnd(call, url, proxies) - } - - override fun proxySelectStart(call: Call, url: HttpUrl) { - delegate.proxySelectStart(call, url) - } - - override fun requestBodyEnd(call: Call, byteCount: Long) { - delegate.requestBodyEnd(call, byteCount) - } - - override fun requestBodyStart(call: Call) { - delegate.requestBodyStart(call) - } - - override fun requestFailed(call: Call, ioe: IOException) { - delegate.requestFailed(call, ioe) - } - - override fun requestHeadersEnd(call: Call, request: Request) { - delegate.requestHeadersEnd(call, request) - } - - override fun requestHeadersStart(call: Call) { - delegate.requestHeadersStart(call) - } - - override fun responseBodyEnd(call: Call, byteCount: Long) { - delegate.responseBodyEnd(call, byteCount) - } - - override fun responseBodyStart(call: Call) { - delegate.responseBodyStart(call) - } - - override fun responseFailed(call: Call, ioe: IOException) { - delegate.responseFailed(call, ioe) - } - - override fun responseHeadersEnd(call: Call, response: Response) { - delegate.responseHeadersEnd(call, response) - } - - override fun responseHeadersStart(call: Call) { - delegate.responseHeadersStart(call) - } - - override fun satisfactionFailure(call: Call, response: Response) { - delegate.satisfactionFailure(call, response) - } - - override fun secureConnectEnd(call: Call, handshake: Handshake?) { - delegate.secureConnectEnd(call, handshake) - } - - override fun secureConnectStart(call: Call) { - delegate.secureConnectStart(call) - } -} 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 deleted file mode 100644 index 6e58b7bb106..00000000000 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ /dev/null @@ -1,79 +0,0 @@ -package io.sentry.android.okhttp - -import io.sentry.HttpStatusCodeRange -import io.sentry.HubAdapter -import io.sentry.IHub -import io.sentry.ISpan -import io.sentry.SentryIntegrationPackageStorage -import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS -import io.sentry.android.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback -import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response - -/** - * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span - * out of the active span bound to the scope for each HTTP Request. - * If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well. - * - * @param hub The [IHub], internal and only used for testing. - * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. - * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, - * Defaults to true. - * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response - * status code is within the defined ranges. - * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL - * is a match for any of the defined targets. - */ -@Deprecated( - "Use SentryOkHttpInterceptor from sentry-okhttp instead", - ReplaceWith("SentryOkHttpInterceptor", "io.sentry.okhttp.SentryOkHttpInterceptor") -) -class SentryOkHttpInterceptor( - private val hub: IHub = HubAdapter.getInstance(), - private val beforeSpan: BeforeSpanCallback? = null, - private val captureFailedRequests: Boolean = true, - private val failedRequestStatusCodes: List = listOf( - HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) - ), - private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : Interceptor by io.sentry.okhttp.SentryOkHttpInterceptor( - hub, - { span, request, response -> - beforeSpan ?: return@SentryOkHttpInterceptor span - beforeSpan.execute(span, request, response) - }, - captureFailedRequests, - failedRequestStatusCodes, - failedRequestTargets -) { - - constructor() : this(HubAdapter.getInstance()) - constructor(hub: IHub) : this(hub, null) - constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) - - init { - addIntegrationToSdkVersion("OkHttp") - SentryIntegrationPackageStorage.getInstance() - .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) - } - - /** - * The BeforeSpan callback - */ - @Deprecated( - "Use BeforeSpanCallback from sentry-okhttp instead", - ReplaceWith("BeforeSpanCallback", "io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback") - ) - fun interface BeforeSpanCallback { - /** - * Mutates or drops span before being added - * - * @param span the span to mutate or drop - * @param request the HTTP request executed by okHttp - * @param response the HTTP response received by okHttp - */ - fun execute(span: ISpan, request: Request, response: Response?): ISpan? - } -} diff --git a/sentry-android-okhttp/src/main/res/values/public.xml b/sentry-android-okhttp/src/main/res/values/public.xml deleted file mode 100644 index 379be515be2..00000000000 --- a/sentry-android-okhttp/src/main/res/values/public.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt deleted file mode 100644 index 9ed110ef7eb..00000000000 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.sentry.android.okhttp - -import io.sentry.IHub -import io.sentry.SentryOptions -import io.sentry.SentryTracer -import io.sentry.TransactionContext -import okhttp3.EventListener -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.SocketPolicy -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever -import kotlin.test.Test -import kotlin.test.assertEquals - -@SuppressWarnings("Deprecated") -class SentryOkHttpEventListenerTest { - - class Fixture { - val hub = mock() - val server = MockWebServer() - lateinit var sentryTracer: SentryTracer - - @SuppressWarnings("LongParameterList") - fun getSut( - eventListener: EventListener? = null - ): OkHttpClient { - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - } - whenever(hub.options).thenReturn(options) - - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) - whenever(hub.span).thenReturn(sentryTracer) - server.enqueue( - MockResponse() - .setBody("responseBody") - .setSocketPolicy(SocketPolicy.KEEP_OPEN) - .setResponseCode(200) - ) - - val builder = OkHttpClient.Builder().addInterceptor(SentryOkHttpInterceptor(hub)) - val sentryOkHttpEventListener = when { - eventListener != null -> SentryOkHttpEventListener(hub, eventListener) - else -> SentryOkHttpEventListener(hub) - } - return builder.eventListener(sentryOkHttpEventListener).build() - } - } - - private val fixture = Fixture() - - private fun getRequest(url: String = "/hello"): Request { - return Request.Builder() - .addHeader("myHeader", "myValue") - .get() - .url(fixture.server.url(url)) - .build() - } - - @Test - fun `when there are multiple SentryOkHttpEventListeners, they don't duplicate spans`() { - val sut = fixture.getSut(eventListener = SentryOkHttpEventListener(fixture.hub)) - val call = sut.newCall(getRequest()) - call.execute().close() - assertEquals(8, fixture.sentryTracer.children.size) - } -} diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 33043e69b66..a7f7931326a 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -76,7 +76,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/ public fun onScreenshotRecorded (Ljava/io/File;J)V public fun onTouchEvent (Landroid/view/MotionEvent;)V public fun pause ()V - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V public fun resume ()V public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V public fun start ()V diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 15713bb6f43..7f1424096c4 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -18,7 +18,7 @@ android { defaultConfig { targetSdk = Config.Android.targetSdkVersion - minSdk = Config.Android.minSdkVersionReplay + minSdk = Config.Android.minSdkVersion testInstrumentationRunner = Config.TestLibs.androidJUnitRunner diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt index 88638e7e168..d67605505d1 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt @@ -15,6 +15,7 @@ import io.sentry.android.replay.video.MuxerConfig import io.sentry.android.replay.video.SimpleVideoEncoder import io.sentry.protocol.SentryId import io.sentry.rrweb.RRWebEvent +import io.sentry.util.AutoClosableReentrantLock import io.sentry.util.FileUtils import java.io.Closeable import java.io.File @@ -42,7 +43,8 @@ public class ReplayCache( ) : Closeable { private val isClosed = AtomicBoolean(false) - private val encoderLock = Any() + private val encoderLock = AutoClosableReentrantLock() + private val lock = AutoClosableReentrantLock() private var encoder: SimpleVideoEncoder? = null internal val replayCacheDir: File? by lazy { @@ -147,7 +149,7 @@ public class ReplayCache( return null } - encoder = synchronized(encoderLock) { + encoder = encoderLock.acquire().use { SimpleVideoEncoder( options, MuxerConfig( @@ -201,7 +203,7 @@ public class ReplayCache( } var videoDuration: Long - synchronized(encoderLock) { + encoderLock.acquire().use { encoder?.release() videoDuration = encoder?.duration ?: 0 encoder = null @@ -218,7 +220,7 @@ public class ReplayCache( } return try { val bitmap = BitmapFactory.decodeFile(frame.screenshot.absolutePath) - synchronized(encoderLock) { + encoderLock.acquire().use { encoder?.encode(bitmap) } bitmap.recycle() @@ -260,7 +262,7 @@ public class ReplayCache( } override fun close() { - synchronized(encoderLock) { + encoderLock.acquire().use { encoder?.release() encoder = null } @@ -268,25 +270,26 @@ public class ReplayCache( } // TODO: it's awful, choose a better serialization format - @Synchronized fun persistSegmentValues(key: String, value: String?) { - if (isClosed.get()) { - return - } - if (ongoingSegment.isEmpty()) { - ongoingSegmentFile?.useLines { lines -> - lines.associateTo(ongoingSegment) { - val (k, v) = it.split("=", limit = 2) - k to v + lock.acquire().use { + if (isClosed.get()) { + return + } + if (ongoingSegment.isEmpty()) { + ongoingSegmentFile?.useLines { lines -> + lines.associateTo(ongoingSegment) { + val (k, v) = it.split("=", limit = 2) + k to v + } } } + if (value == null) { + ongoingSegment.remove(key) + } else { + ongoingSegment[key] = value + } + ongoingSegmentFile?.writeText(ongoingSegment.entries.joinToString("\n") { (k, v) -> "$k=$v" }) } - if (value == null) { - ongoingSegment.remove(key) - } else { - ongoingSegment[key] = value - } - ongoingSegmentFile?.writeText(ongoingSegment.entries.joinToString("\n") { (k, v) -> "$k=$v" }) } companion object { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index c0b77abc2a5..07ecf47756f 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -12,7 +12,7 @@ import io.sentry.DataCategory.Replay import io.sentry.IConnectionStatusProvider.ConnectionStatus import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED import io.sentry.IConnectionStatusProvider.IConnectionStatusObserver -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Integration import io.sentry.NoOpReplayBreadcrumbConverter import io.sentry.ReplayBreadcrumbConverter @@ -91,7 +91,7 @@ public class ReplayIntegration( } private lateinit var options: SentryOptions - private var hub: IHub? = null + private var scopes: IScopes? = null private var recorder: Recorder? = null private var gestureRecorder: GestureRecorder? = null private val random by lazy { Random() } @@ -110,7 +110,7 @@ public class ReplayIntegration( private var mainLooperHandler: MainLooperHandler = MainLooperHandler() private var gestureRecorderProvider: (() -> GestureRecorder)? = null - override fun register(hub: IHub, options: SentryOptions) { + override fun register(scopes: IScopes, options: SentryOptions) { this.options = options if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { @@ -125,13 +125,13 @@ public class ReplayIntegration( return } - this.hub = hub + this.scopes = scopes recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, mainLooperHandler, replayExecutor) gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this) isEnabled.set(true) options.connectionStatusProvider.addConnectionStatusObserver(this) - hub.rateLimiter?.addRateLimitObserver(this) + scopes.rateLimiter?.addRateLimitObserver(this) if (options.sessionReplay.isTrackOrientationChange) { try { context.registerComponentCallbacks(this) @@ -175,9 +175,9 @@ public class ReplayIntegration( val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.sessionReplay) captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) { - SessionCaptureStrategy(options, hub, dateProvider, replayExecutor, replayCacheProvider) + SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, replayCacheProvider) } else { - BufferCaptureStrategy(options, hub, dateProvider, random, replayExecutor, replayCacheProvider) + BufferCaptureStrategy(options, scopes, dateProvider, random, replayExecutor, replayCacheProvider) } captureStrategy?.start(recorderConfig) @@ -243,7 +243,7 @@ public class ReplayIntegration( override fun onScreenshotRecorded(bitmap: Bitmap) { var screen: String? = null - hub?.configureScope { screen = it.screen?.substringAfterLast('.') } + scopes?.configureScope { screen = it.screen?.substringAfterLast('.') } captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp -> addFrame(bitmap, frameTimeStamp, screen) checkCanRecord() @@ -263,7 +263,7 @@ public class ReplayIntegration( } options.connectionStatusProvider.removeConnectionStatusObserver(this) - hub?.rateLimiter?.removeRateLimitObserver(this) + scopes?.rateLimiter?.removeRateLimitObserver(this) if (options.sessionReplay.isTrackOrientationChange) { try { context.unregisterComponentCallbacks(this) @@ -332,8 +332,8 @@ public class ReplayIntegration( if (captureStrategy is SessionCaptureStrategy && ( options.connectionStatusProvider.connectionStatus == DISCONNECTED || - hub?.rateLimiter?.isActiveForCategory(All) == true || - hub?.rateLimiter?.isActiveForCategory(Replay) == true + scopes?.rateLimiter?.isActiveForCategory(All) == true || + scopes?.rateLimiter?.isActiveForCategory(Replay) == true ) ) { pause() @@ -389,7 +389,7 @@ public class ReplayIntegration( } val breadcrumbs = PersistingScopeObserver.read(options, BREADCRUMBS_FILENAME, List::class.java, Breadcrumb.Deserializer()) as? List val segment = CaptureStrategy.createSegment( - hub = hub, + scopes = scopes, options = options, duration = lastSegment.duration, currentSegmentTimestamp = lastSegment.timestamp, @@ -408,7 +408,7 @@ public class ReplayIntegration( if (segment is ReplaySegment.Created) { val hint = HintUtils.createWithTypeCheckHint(PreviousReplayHint()) - segment.capture(hub, hint) + segment.capture(scopes, hint) } cleanupReplays(unfinishedReplayId = previousReplayIdString) // will be cleaned up after the envelope is assembled } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index ba3cfc71155..62752c74cc3 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -33,7 +33,6 @@ import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.TextViewHierarch import java.io.File import java.lang.ref.WeakReference import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicBoolean import kotlin.LazyThreadSafetyMode.NONE import kotlin.math.roundToInt @@ -243,15 +242,6 @@ internal class ScreenshotRecorder( // get the pixel color (= dominant color) return singlePixelBitmap.getPixel(0, 0) } - - private class RecorderExecutorServiceThreadFactory : ThreadFactory { - private var cnt = 0 - override fun newThread(r: Runnable): Thread { - val ret = Thread(r, "SentryReplayRecorder-" + cnt++) - ret.setDaemon(true) - return ret - } - } } public data class ScreenshotRecorderConfig( diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt index 8f4b0526fcb..a5de0f9a2c3 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt @@ -6,6 +6,7 @@ import io.sentry.SentryOptions import io.sentry.android.replay.util.MainLooperHandler import io.sentry.android.replay.util.gracefullyShutdown import io.sentry.android.replay.util.scheduleAtFixedRateSafely +import io.sentry.util.AutoClosableReentrantLock import java.lang.ref.WeakReference import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService @@ -28,7 +29,7 @@ internal class WindowRecorder( private val isRecording = AtomicBoolean(false) private val rootViews = ArrayList>() - private val rootViewsLock = Any() + private val rootViewsLock = AutoClosableReentrantLock() private var recorder: ScreenshotRecorder? = null private var capturingTask: ScheduledFuture<*>? = null private val capturer by lazy { @@ -36,7 +37,7 @@ internal class WindowRecorder( } override fun onRootViewsChanged(root: View, added: Boolean) { - synchronized(rootViewsLock) { + rootViewsLock.acquire().use { if (added) { rootViews.add(WeakReference(root)) recorder?.bind(root) @@ -81,7 +82,7 @@ internal class WindowRecorder( } override fun stop() { - synchronized(rootViewsLock) { + rootViewsLock.acquire().use { rootViews.forEach { recorder?.unbind(it.get()) } rootViews.clear() } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt index c3eea18e437..18f6ece8b5e 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt @@ -25,6 +25,7 @@ import android.os.Looper import android.util.Log import android.view.View import android.view.Window +import io.sentry.util.AutoClosableReentrantLock import java.io.Closeable import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean @@ -123,11 +124,11 @@ internal fun interface OnRootViewsChangedListener { internal class RootViewsSpy private constructor() : Closeable { private val isClosed = AtomicBoolean(false) - private val viewListLock = Any() + private val viewListLock = AutoClosableReentrantLock() val listeners: CopyOnWriteArrayList = object : CopyOnWriteArrayList() { override fun add(element: OnRootViewsChangedListener?): Boolean { - synchronized(viewListLock) { + viewListLock.acquire().use { // notify listener about existing root views immediately delegatingViewList.forEach { element?.onRootViewsChanged(it, true) @@ -174,7 +175,7 @@ internal class RootViewsSpy private constructor() : Closeable { return@postAtFrontOfQueue } WindowManagerSpy.swapWindowManagerGlobalMViews { mViews -> - synchronized(viewListLock) { + viewListLock.acquire().use { delegatingViewList.apply { addAll(mViews) } } } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt index fbc80565b1b..776348ed98d 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt @@ -4,7 +4,7 @@ import android.annotation.TargetApi import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType import io.sentry.SentryReplayEvent.ReplayType.BUFFER @@ -43,7 +43,7 @@ import kotlin.reflect.KProperty @TargetApi(26) internal abstract class BaseCaptureStrategy( private val options: SentryOptions, - private val hub: IHub?, + private val scopes: IScopes?, private val dateProvider: ICurrentDateProvider, protected val replayExecutor: ScheduledExecutorService, private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null @@ -130,7 +130,7 @@ internal abstract class BaseCaptureStrategy( events: Deque = this.currentEvents ): ReplaySegment = createSegment( - hub, + scopes, options, duration, currentSegmentTimestamp, @@ -178,7 +178,7 @@ internal abstract class BaseCaptureStrategy( private val value = AtomicReference(initialValue) private fun runInBackground(task: () -> Unit) { - if (options.mainThreadChecker.isMainThread) { + if (options.threadChecker.isMainThread) { persistingExecutor.submitSafely(options, "$TAG.runInBackground") { task() } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index 996e31afbd8..e0ec0a91e22 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -4,7 +4,7 @@ import android.annotation.TargetApi import android.graphics.Bitmap import android.view.MotionEvent import io.sentry.DateUtils -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.ERROR import io.sentry.SentryLevel.INFO @@ -27,12 +27,12 @@ import java.util.concurrent.ScheduledExecutorService @TargetApi(26) internal class BufferCaptureStrategy( private val options: SentryOptions, - private val hub: IHub?, + private val scopes: IScopes?, private val dateProvider: ICurrentDateProvider, private val random: Random, executor: ScheduledExecutorService, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null -) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider = replayCacheProvider) { +) : BaseCaptureStrategy(options, scopes, dateProvider, executor, replayCacheProvider = replayCacheProvider) { // TODO: capture envelopes for buffered segments instead, but don't send them until buffer is triggered private val bufferedSegments = mutableListOf() @@ -74,7 +74,7 @@ internal class BufferCaptureStrategy( // write replayId to scope right away, so it gets picked up by the event that caused buffer // to flush - hub?.configureScope { + scopes?.configureScope { it.replayId = currentReplayId } @@ -89,7 +89,7 @@ internal class BufferCaptureStrategy( bufferedSegments.capture() if (segment is ReplaySegment.Created) { - segment.capture(hub) + segment.capture(scopes) // we only want to increment segment_id in the case of success, but currentSegment // might be irrelevant since we changed strategies, so in the callback we increment @@ -130,7 +130,7 @@ internal class BufferCaptureStrategy( return this } // we hand over replayExecutor to the new strategy to preserve order of execution - val captureStrategy = SessionCaptureStrategy(options, hub, dateProvider, replayExecutor) + val captureStrategy = SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor) captureStrategy.start(recorderConfig, segmentId = currentSegment, replayId = currentReplayId, replayType = BUFFER) return captureStrategy } @@ -157,7 +157,7 @@ internal class BufferCaptureStrategy( private fun MutableList.capture() { var bufferedSegment = removeFirstOrNull() while (bufferedSegment != null) { - bufferedSegment.capture(hub) + bufferedSegment.capture(scopes) bufferedSegment = removeFirstOrNull() // a short delay between processing envelopes to avoid bursting our server and hitting // another rate limit https://develop.sentry.dev/sdk/features/#additional-capabilities diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt index 660a366ecd2..98007c45538 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt @@ -5,7 +5,7 @@ import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ReplayRecording import io.sentry.SentryOptions import io.sentry.SentryReplayEvent @@ -59,7 +59,7 @@ internal interface CaptureStrategy { private const val BREADCRUMB_START_OFFSET = 100L fun createSegment( - hub: IHub?, + scopes: IScopes?, options: SentryOptions, duration: Long, currentSegmentTimestamp: Date, @@ -89,7 +89,7 @@ internal interface CaptureStrategy { val replayBreadcrumbs: List = if (breadcrumbs == null) { var crumbs = emptyList() - hub?.configureScope { scope -> + scopes?.configureScope { scope -> crumbs = ArrayList(scope.breadcrumbs) } crumbs @@ -234,8 +234,8 @@ internal interface CaptureStrategy { val replay: SentryReplayEvent, val recording: ReplayRecording ) : ReplaySegment() { - fun capture(hub: IHub?, hint: Hint = Hint()) { - hub?.captureReplay(replay, hint.apply { replayRecording = recording }) + fun capture(scopes: IScopes?, hint: Hint = Hint()) { + scopes?.captureReplay(replay, hint.apply { replayRecording = recording }) } fun setSegmentId(segmentId: Int) { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index 03ca0cdf552..5eefafa25a7 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -1,7 +1,7 @@ package io.sentry.android.replay.capture import android.graphics.Bitmap -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions @@ -18,11 +18,11 @@ import java.util.concurrent.ScheduledExecutorService internal class SessionCaptureStrategy( private val options: SentryOptions, - private val hub: IHub?, + private val scopes: IScopes?, private val dateProvider: ICurrentDateProvider, executor: ScheduledExecutorService, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null -) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider) { +) : BaseCaptureStrategy(options, scopes, dateProvider, executor, replayCacheProvider) { internal companion object { private const val TAG = "SessionCaptureStrategy" @@ -37,7 +37,7 @@ internal class SessionCaptureStrategy( super.start(recorderConfig, segmentId, replayId, replayType) // only set replayId on the scope if it's a full session, otherwise all events will be // tagged with the replay that might never be sent when we're recording in buffer mode - hub?.configureScope { + scopes?.configureScope { it.replayId = currentReplayId screenAtStart = it.screen?.substringAfterLast('.') } @@ -46,7 +46,7 @@ internal class SessionCaptureStrategy( override fun pause() { createCurrentSegment("pause") { segment -> if (segment is ReplaySegment.Created) { - segment.capture(hub) + segment.capture(scopes) currentSegment++ } @@ -58,11 +58,11 @@ internal class SessionCaptureStrategy( val replayCacheDir = cache?.replayCacheDir createCurrentSegment("stop") { segment -> if (segment is ReplaySegment.Created) { - segment.capture(hub) + segment.capture(scopes) } FileUtils.deleteRecursively(replayCacheDir) } - hub?.configureScope { it.replayId = SentryId.EMPTY_ID } + scopes?.configureScope { it.replayId = SentryId.EMPTY_ID } super.stop() } @@ -103,7 +103,7 @@ internal class SessionCaptureStrategy( width ) if (segment is ReplaySegment.Created) { - segment.capture(hub) + segment.capture(scopes) currentSegment++ // set next segment timestamp as close to the previous one as possible to avoid gaps segmentTimestamp = segment.replay.timestamp @@ -120,7 +120,7 @@ internal class SessionCaptureStrategy( override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) { createCurrentSegment("onConfigurationChanged") { segment -> if (segment is ReplaySegment.Created) { - segment.capture(hub) + segment.capture(scopes) currentSegment++ // set next segment timestamp as close to the previous one as possible to avoid gaps diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt index 7e80f26e4fc..a8da77d8511 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/gestures/GestureRecorder.kt @@ -9,6 +9,7 @@ import io.sentry.SentryOptions import io.sentry.android.replay.OnRootViewsChangedListener import io.sentry.android.replay.phoneWindow import io.sentry.android.replay.util.FixedWindowCallback +import io.sentry.util.AutoClosableReentrantLock import java.lang.ref.WeakReference class GestureRecorder( @@ -17,10 +18,10 @@ class GestureRecorder( ) : OnRootViewsChangedListener { private val rootViews = ArrayList>() - private val rootViewsLock = Any() + private val rootViewsLock = AutoClosableReentrantLock() override fun onRootViewsChanged(root: View, added: Boolean) { - synchronized(rootViewsLock) { + rootViewsLock.acquire().use { if (added) { rootViews.add(WeakReference(root)) root.startGestureTracking() @@ -32,7 +33,7 @@ class GestureRecorder( } fun stop() { - synchronized(rootViewsLock) { + rootViewsLock.acquire().use { rootViews.forEach { it.get()?.stopGestureTracking() } rootViews.clear() } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt index 2ae3bad1b6d..ab11629c2a4 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt @@ -42,7 +42,7 @@ internal class PersistableLinkedList( private fun persistRecording() { val cache = cacheProvider() ?: return val recording = ReplayRecording().apply { payload = ArrayList(this@PersistableLinkedList) } - if (options.mainThreadChecker.isMainThread) { + if (options.threadChecker.isMainThread) { persistingExecutor.submit { val stringWriter = StringWriter() options.serializer.serialize(recording, BufferedWriter(stringWriter)) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/AnrWithReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/AnrWithReplayIntegrationTest.kt index 510f87715f5..104833bb126 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/AnrWithReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/AnrWithReplayIntegrationTest.kt @@ -16,6 +16,7 @@ import io.sentry.SentryReplayEvent import io.sentry.SentryReplayEvent.ReplayType import io.sentry.SystemOutLogger import io.sentry.android.core.SentryAndroid +import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.replay.ReplayCache.Companion.ONGOING_SEGMENT import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_BIT_RATE @@ -32,6 +33,7 @@ import io.sentry.protocol.Contexts import io.sentry.protocol.SentryId import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent +import io.sentry.test.applyTestOptions import org.awaitility.kotlin.await import org.awaitility.kotlin.withAlias import org.junit.Rule @@ -146,7 +148,7 @@ class AnrWithReplayIntegrationTest { val replayId1 = SentryId() val replayId2 = SentryId() - SentryAndroid.init(context) { + initForTest(context) { it.dsn = "https://key@sentry.io/123" it.cacheDirPath = cacheDir it.isDebug = true @@ -216,3 +218,10 @@ class AnrWithReplayIntegrationTest { .untilTrue(asserted) } } + +fun initForTest(context: Context, optionsConfiguration: Sentry.OptionsConfiguration) { + SentryAndroid.init(context) { + applyTestOptions(it) + optionsConfiguration.configure(it) + } +} diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index 4183a780b8e..747519943a3 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -11,7 +11,7 @@ import io.sentry.DateUtils import io.sentry.Hint import io.sentry.IConnectionStatusProvider.ConnectionStatus.CONNECTED import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryEvent @@ -87,7 +87,7 @@ class ReplayIntegrationTest { } val scope = Scope(options) val rateLimiter = mock() - val hub = mock { + val scopes = mock { doAnswer { ((it.arguments[0]) as ScopeCallback).run(scope) }.whenever(mock).configureScope(any()) @@ -148,7 +148,7 @@ class ReplayIntegrationTest { fun `when API is below 26, does not register`() { val replay = fixture.getSut(context) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertFalse(replay.isEnabled.get()) } @@ -157,7 +157,7 @@ class ReplayIntegrationTest { fun `when no sample rate is set, does not register`() { val replay = fixture.getSut(context, 0.0, 0.0) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertFalse(replay.isEnabled.get()) } @@ -170,7 +170,7 @@ class ReplayIntegrationTest { mock() }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertTrue(replay.isEnabled.get()) assertTrue(recorderCreated) @@ -192,7 +192,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() assertTrue(replay.isRecording) @@ -203,7 +203,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.start() @@ -220,7 +220,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, onErrorSampleRate = 0.0, sessionSampleRate = 0.0, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() verify(captureStrategy, never()).start( @@ -236,7 +236,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, sessionSampleRate = 0.0, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() verify(captureStrategy, times(1)).start( @@ -252,7 +252,7 @@ class ReplayIntegrationTest { val recorder = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() verify(recorder).start(any()) @@ -263,7 +263,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.resume() verify(captureStrategy, never()).resume() @@ -275,7 +275,7 @@ class ReplayIntegrationTest { val recorder = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.resume() @@ -288,7 +288,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) val event = SentryEvent().apply { exceptions = listOf(SentryException()) @@ -305,7 +305,7 @@ class ReplayIntegrationTest { } val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() val event = SentryEvent().apply { @@ -323,7 +323,7 @@ class ReplayIntegrationTest { } val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() val id = SentryId() @@ -343,7 +343,7 @@ class ReplayIntegrationTest { val captureStrategy = mock() val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.pause() verify(captureStrategy, never()).pause() @@ -355,7 +355,7 @@ class ReplayIntegrationTest { val recorder = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.pause() @@ -369,7 +369,7 @@ class ReplayIntegrationTest { val recorder = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.stop() verify(captureStrategy, never()).stop() @@ -388,7 +388,7 @@ class ReplayIntegrationTest { gestureRecorderProvider = { gestureRecorder } ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.stop() @@ -403,7 +403,7 @@ class ReplayIntegrationTest { val recorder = mock() val captureStrategy = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.close() @@ -420,7 +420,7 @@ class ReplayIntegrationTest { val recorder = mock() val replay = fixture.getSut(context, recorderProvider = { recorder }, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.onConfigurationChanged(mock()) verify(captureStrategy, never()).onConfigurationChanged(any()) @@ -440,7 +440,7 @@ class ReplayIntegrationTest { recorderConfigProvider = { configChanged = it; recorderConfig } ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onConfigurationChanged(mock()) @@ -498,10 +498,10 @@ class ReplayIntegrationTest { } val replay = fixture.getSut(context) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertTrue(oldReplay.exists()) // should not be deleted until the video is packed into envelope - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals(oldReplayId, it.replayId) assertEquals(ReplayType.SESSION, it.replayType) @@ -557,7 +557,7 @@ class ReplayIntegrationTest { on { currentReplayId }.thenReturn(replayId) } val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertTrue(scopeCache.exists()) assertFalse(evenOlderReplay.exists()) @@ -572,8 +572,8 @@ class ReplayIntegrationTest { } val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy }) - fixture.hub.configureScope { it.screen = "MainActivity" } - replay.register(fixture.hub, fixture.options) + fixture.scopes.configureScope { it.screen = "MainActivity" } + replay.register(fixture.scopes, fixture.options) replay.start() replay.onScreenshotRecorded(mock()) @@ -592,7 +592,7 @@ class ReplayIntegrationTest { isOffline = true ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onScreenshotRecorded(mock()) @@ -610,7 +610,7 @@ class ReplayIntegrationTest { isRateLimited = true ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onScreenshotRecorded(mock()) @@ -627,7 +627,7 @@ class ReplayIntegrationTest { replayCaptureStrategyProvider = { captureStrategy } ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onConnectionStatusChanged(DISCONNECTED) @@ -644,7 +644,7 @@ class ReplayIntegrationTest { replayCaptureStrategyProvider = { captureStrategy } ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onConnectionStatusChanged(CONNECTED) @@ -662,7 +662,7 @@ class ReplayIntegrationTest { isRateLimited = true ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onRateLimitChanged(fixture.rateLimiter) @@ -680,7 +680,7 @@ class ReplayIntegrationTest { isRateLimited = false ) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() replay.onRateLimitChanged(fixture.rateLimiter) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt index ae817a17596..25713ad2959 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt @@ -6,7 +6,7 @@ import android.graphics.Bitmap.CompressFormat.JPEG import android.graphics.Bitmap.Config.ARGB_8888 import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType import io.sentry.android.replay.ReplayIntegrationWithRecorderTest.LifecycleState.CLOSED @@ -20,7 +20,7 @@ import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider -import io.sentry.util.thread.NoOpMainThreadChecker +import io.sentry.util.thread.NoOpThreadChecker import org.awaitility.kotlin.await import org.junit.Rule import org.junit.Test @@ -50,9 +50,9 @@ class ReplayIntegrationWithRecorderTest { internal class Fixture { val options = SentryOptions().apply { - mainThreadChecker = NoOpMainThreadChecker.getInstance() + threadChecker = NoOpThreadChecker.getInstance() } - val hub = mock() + val scopes = mock() fun getSut( context: Context, @@ -81,7 +81,7 @@ class ReplayIntegrationWithRecorderTest { @Test fun `works with different recorder`() { val captured = AtomicBoolean(false) - whenever(fixture.hub.captureReplay(any(), anyOrNull())).then { + whenever(fixture.scopes.captureReplay(any(), anyOrNull())).then { captured.set(true) } // fake current time to trigger segment creation, CurrentDateProvider.getInstance() should @@ -120,7 +120,7 @@ class ReplayIntegrationWithRecorderTest { } replay = fixture.getSut(context, recorder, recorderConfig, dateProvider) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) assertEquals(INITALIZED, recorder.state) @@ -152,7 +152,7 @@ class ReplayIntegrationWithRecorderTest { // verify await.untilTrue(captured) - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals(replay.replayId, it.replayId) assertEquals(ReplayType.SESSION, it.replayType) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt index 9bd8e2038dc..8a7aa6611d7 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplaySmokeTest.kt @@ -13,7 +13,7 @@ import android.widget.LinearLayout.LayoutParams import android.widget.TextView import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -61,7 +61,7 @@ class ReplaySmokeTest { internal class Fixture { val options = SentryOptions() val scope = Scope(options) - val hub = mock { + val scopes = mock { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) @@ -104,7 +104,7 @@ class ReplaySmokeTest { @Test fun `works in session mode`() { val captured = AtomicBoolean(false) - whenever(fixture.hub.captureReplay(any(), anyOrNull())).then { + whenever(fixture.scopes.captureReplay(any(), anyOrNull())).then { captured.set(true) } @@ -112,7 +112,7 @@ class ReplaySmokeTest { fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath val replay: ReplayIntegration = fixture.getSut(context) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) val controller = buildActivity(ExampleActivity::class.java, null).setup() controller.create().start().resume() @@ -123,7 +123,7 @@ class ReplaySmokeTest { await.timeout(Duration.ofSeconds(15)).untilTrue(captured) - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals(replay.replayId, it.replayId) assertEquals(ReplayType.SESSION, it.replayType) @@ -151,7 +151,7 @@ class ReplaySmokeTest { ReplayShadowMediaCodec.framesToEncode = 10 val captured = AtomicBoolean(false) - whenever(fixture.hub.captureReplay(any(), anyOrNull())).then { + whenever(fixture.scopes.captureReplay(any(), anyOrNull())).then { captured.set(true) } @@ -159,7 +159,7 @@ class ReplaySmokeTest { fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath val replay: ReplayIntegration = fixture.getSut(context) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) val controller = buildActivity(ExampleActivity::class.java, null).setup() controller.create().start().resume() @@ -178,7 +178,7 @@ class ReplaySmokeTest { await.timeout(Duration.ofSeconds(5)).untilTrue(captured) - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals(replay.replayId, it.replayId) assertEquals(ReplayType.BUFFER, it.replayType) @@ -208,7 +208,7 @@ class ReplaySmokeTest { fixture.options.cacheDirPath = tmpDir.newFolder().absolutePath // first init + close - val falseHub = mock { + val falseHub = mock { doAnswer { (it.arguments[0] as ScopeCallback).run(fixture.scope) }.whenever(it).configureScope(any()) @@ -220,11 +220,11 @@ class ReplaySmokeTest { // second init val captured = AtomicBoolean(false) - whenever(fixture.hub.captureReplay(any(), anyOrNull())).then { + whenever(fixture.scopes.captureReplay(any(), anyOrNull())).then { captured.set(true) } val replay: ReplayIntegration = fixture.getSut(context) - replay.register(fixture.hub, fixture.options) + replay.register(fixture.scopes, fixture.options) replay.start() val controller = buildActivity(ExampleActivity::class.java, null).setup() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index 29c777e171f..863d5c85f8b 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -2,7 +2,7 @@ package io.sentry.android.replay.capture import android.graphics.Bitmap import android.view.MotionEvent -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -59,7 +59,7 @@ class BufferCaptureStrategyTest { ) } val scope = Scope(options) - val hub = mock { + val scopes = mock { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) @@ -95,7 +95,7 @@ class BufferCaptureStrategyTest { } return BufferCaptureStrategy( options, - hub, + scopes, dateProvider, Random(), mock { @@ -155,7 +155,7 @@ class BufferCaptureStrategyTest { await.until { strategy.currentSegment == 1 } - verify(fixture.hub, never()).captureReplay(any(), any()) + verify(fixture.scopes, never()).captureReplay(any(), any()) assertEquals(1, strategy.currentSegment) } @@ -170,7 +170,7 @@ class BufferCaptureStrategyTest { strategy.stop() - verify(fixture.hub, never()).captureReplay(any(), any()) + verify(fixture.scopes, never()).captureReplay(any(), any()) assertEquals(SentryId.EMPTY_ID, strategy.currentReplayId) assertEquals(-1, strategy.currentSegment) @@ -217,7 +217,7 @@ class BufferCaptureStrategyTest { await.until { strategy.currentSegment == 1 } - verify(fixture.hub, never()).captureReplay(any(), any()) + verify(fixture.scopes, never()).captureReplay(any(), any()) assertEquals(1, strategy.currentSegment) } @@ -276,7 +276,7 @@ class BufferCaptureStrategyTest { } // buffered + current = 2 - verify(fixture.hub, times(2)).captureReplay(any(), any()) + verify(fixture.scopes, times(2)).captureReplay(any(), any()) assertEquals(strategy.currentReplayId, fixture.scope.replayId) assertTrue(called) } @@ -291,7 +291,7 @@ class BufferCaptureStrategyTest { assertEquals(oldTimestamp!!.time + VIDEO_DURATION, newTimestamp.time) } - verify(fixture.hub).captureReplay(any(), any()) + verify(fixture.scopes).captureReplay(any(), any()) } @Test diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index dfe4137fb06..79afdb8f853 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -3,7 +3,7 @@ package io.sentry.android.replay.capture import android.graphics.Bitmap import io.sentry.Breadcrumb import io.sentry.DateUtils -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -71,7 +71,7 @@ class SessionCaptureStrategyTest { ) } val scope = Scope(options) - val hub = mock { + val scopes = mock { doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) @@ -103,7 +103,7 @@ class SessionCaptureStrategyTest { } return SessionCaptureStrategy( options, - hub, + scopes, dateProvider, mock { doAnswer { invocation -> @@ -168,7 +168,7 @@ class SessionCaptureStrategyTest { strategy.pause() - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, @@ -188,7 +188,7 @@ class SessionCaptureStrategyTest { strategy.stop() - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, @@ -208,7 +208,7 @@ class SessionCaptureStrategyTest { strategy.captureReplay(false) {} - verify(fixture.hub, never()).captureReplay(any(), any()) + verify(fixture.scopes, never()).captureReplay(any(), any()) } @Test @@ -223,7 +223,7 @@ class SessionCaptureStrategyTest { strategy.captureReplay(true) {} strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub, never()).captureReplay(any(), any()) + verify(fixture.scopes, never()).captureReplay(any(), any()) } @Test @@ -240,7 +240,7 @@ class SessionCaptureStrategyTest { } var segmentTimestamp: Date? = null - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> segmentTimestamp = event.replayStartTimestamp event is SentryReplayEvent && event.segmentId == 0 @@ -288,7 +288,7 @@ class SessionCaptureStrategyTest { strategy.onConfigurationChanged(newConfig) var segmentTimestamp: Date? = null - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> segmentTimestamp = event.replayStartTimestamp event is SentryReplayEvent && event.segmentId == 0 @@ -323,7 +323,7 @@ class SessionCaptureStrategyTest { strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals("to", it.urls!!.first()) }, @@ -347,7 +347,7 @@ class SessionCaptureStrategyTest { strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( check { assertEquals("MainActivity", it.urls!!.first()) }, @@ -388,7 +388,7 @@ class SessionCaptureStrategyTest { strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, @@ -431,7 +431,7 @@ class SessionCaptureStrategyTest { strategy.start(fixture.recorderConfig) strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, @@ -439,7 +439,7 @@ class SessionCaptureStrategyTest { ) strategy.onScreenshotRecorded(mock()) {} - verify(fixture.hub).captureReplay( + verify(fixture.scopes).captureReplay( argThat { event -> event is SentryReplayEvent && event.segmentId == 1 }, diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt index c45d31aa171..61c7a771dc7 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt @@ -2,10 +2,10 @@ package io.sentry.android.sqlite import android.database.CrossProcessCursor import android.database.SQLException -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.Instrumenter +import io.sentry.ScopesAdapter import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryStackTraceFactory import io.sentry.SpanDataConvention @@ -14,10 +14,10 @@ import io.sentry.SpanStatus private const val TRACE_ORIGIN = "auto.db.sqlite" internal class SQLiteSpanManager( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), private val databaseName: String? = null ) { - private val stackTraceFactory = SentryStackTraceFactory(hub.options) + private val stackTraceFactory = SentryStackTraceFactory(scopes.options) init { SentryIntegrationPackageStorage.getInstance().addIntegration("SQLite") @@ -33,7 +33,7 @@ internal class SQLiteSpanManager( @Suppress("TooGenericExceptionCaught", "UNCHECKED_CAST") @Throws(SQLException::class) fun performSql(sql: String, operation: () -> T): T { - val startTimestamp = hub.getOptions().dateProvider.now() + val startTimestamp = scopes.getOptions().dateProvider.now() var span: ISpan? = null return try { val result = operation() @@ -45,19 +45,19 @@ internal class SQLiteSpanManager( if (result is CrossProcessCursor) { return SentryCrossProcessCursor(result, this, sql) as T } - span = hub.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) + span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) span?.spanContext?.origin = TRACE_ORIGIN span?.status = SpanStatus.OK result } catch (e: Throwable) { - span = hub.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) + span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) span?.spanContext?.origin = TRACE_ORIGIN span?.status = SpanStatus.INTERNAL_ERROR span?.throwable = e throw e } finally { span?.apply { - val isMainThread: Boolean = hub.options.mainThreadChecker.isMainThread + val isMainThread: Boolean = scopes.options.threadChecker.isMainThread setData(SpanDataConvention.BLOCKED_MAIN_THREAD_KEY, isMainThread) if (isMainThread) { setData(SpanDataConvention.CALL_STACK_KEY, stackTraceFactory.inAppCallStack) diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt index 425932113cd..17c37d69bfc 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt @@ -2,14 +2,14 @@ package io.sentry.android.sqlite import android.database.CrossProcessCursor import android.database.SQLException -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext -import io.sentry.util.thread.IMainThreadChecker +import io.sentry.util.thread.IThreadChecker import org.junit.Before import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -24,7 +24,7 @@ import kotlin.test.assertTrue class SQLiteSpanManagerTest { private class Fixture { - private val hub = mock() + private val scopes = mock() lateinit var sentryTracer: SentryTracer lateinit var options: SentryOptions @@ -32,13 +32,13 @@ class SQLiteSpanManagerTest { options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } - return SQLiteSpanManager(hub, databaseName) + return SQLiteSpanManager(scopes, databaseName) } } @@ -98,8 +98,8 @@ class SQLiteSpanManagerTest { fun `when performSql runs in background blocked_main_thread is false and no stack trace is attached`() { val sut = fixture.getSut() - fixture.options.mainThreadChecker = mock() - whenever(fixture.options.mainThreadChecker.isMainThread).thenReturn(false) + fixture.options.threadChecker = mock() + whenever(fixture.options.threadChecker.isMainThread).thenReturn(false) sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() @@ -112,8 +112,8 @@ class SQLiteSpanManagerTest { fun `when performSql runs in foreground blocked_main_thread is true and a stack trace is attached`() { val sut = fixture.getSut() - fixture.options.mainThreadChecker = mock() - whenever(fixture.options.mainThreadChecker.isMainThread).thenReturn(true) + fixture.options.threadChecker = mock() + whenever(fixture.options.threadChecker.isMainThread).thenReturn(true) sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt index 13ddf4500cc..409e3a5b07a 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt @@ -1,7 +1,7 @@ package io.sentry.android.sqlite import android.database.CrossProcessCursor -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryOptions import io.sentry.SentryTracer @@ -19,8 +19,8 @@ import kotlin.test.assertTrue class SentryCrossProcessCursorTest { private class Fixture { - private val hub = mock() - private val spanManager = SQLiteSpanManager(hub) + private val scopes = mock() + private val spanManager = SQLiteSpanManager(scopes) val mockCursor = mock() lateinit var options: SentryOptions lateinit var sentryTracer: SentryTracer @@ -29,11 +29,11 @@ class SentryCrossProcessCursorTest { options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } return SentryCrossProcessCursor(mockCursor, spanManager, sql) } diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabaseTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabaseTest.kt index cf22c3b0ec3..99e1d5f4a04 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabaseTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabaseTest.kt @@ -3,7 +3,7 @@ package io.sentry.android.sqlite import android.database.Cursor import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQuery -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryOptions import io.sentry.SentryTracer @@ -23,8 +23,8 @@ import kotlin.test.assertTrue class SentrySupportSQLiteDatabaseTest { private class Fixture { - private val hub = mock() - private val spanManager = SQLiteSpanManager(hub) + private val scopes = mock() + private val spanManager = SQLiteSpanManager(scopes) val mockDatabase = mock() lateinit var sentryTracer: SentryTracer lateinit var options: SentryOptions @@ -37,11 +37,11 @@ class SentrySupportSQLiteDatabaseTest { options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } return SentrySupportSQLiteDatabase(mockDatabase, spanManager) diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteStatementTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteStatementTest.kt index 9078ba8b08b..4b6292bd27f 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteStatementTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentrySupportSQLiteStatementTest.kt @@ -1,7 +1,7 @@ package io.sentry.android.sqlite import androidx.sqlite.db.SupportSQLiteStatement -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryOptions import io.sentry.SentryTracer @@ -18,8 +18,8 @@ import kotlin.test.assertTrue class SentrySupportSQLiteStatementTest { private class Fixture { - private val hub = mock() - private val spanManager = SQLiteSpanManager(hub) + private val scopes = mock() + private val spanManager = SQLiteSpanManager(scopes) val mockStatement = mock() lateinit var sentryTracer: SentryTracer lateinit var options: SentryOptions @@ -28,11 +28,11 @@ class SentrySupportSQLiteStatementTest { options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } return SentrySupportSQLiteStatement(mockStatement, spanManager, sql) } diff --git a/sentry-android-timber/api/sentry-android-timber.api b/sentry-android-timber/api/sentry-android-timber.api index 808e91bf105..2d71f67570e 100644 --- a/sentry-android-timber/api/sentry-android-timber.api +++ b/sentry-android-timber/api/sentry-android-timber.api @@ -14,11 +14,11 @@ public final class io/sentry/android/timber/SentryTimberIntegration : io/sentry/ public fun close ()V public final fun getMinBreadcrumbLevel ()Lio/sentry/SentryLevel; public final fun getMinEventLevel ()Lio/sentry/SentryLevel; - public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } public final class io/sentry/android/timber/SentryTimberTree : timber/log/Timber$Tree { - public fun (Lio/sentry/IHub;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V + public fun (Lio/sentry/IScopes;Lio/sentry/SentryLevel;Lio/sentry/SentryLevel;)V public fun d (Ljava/lang/String;[Ljava/lang/Object;)V public fun d (Ljava/lang/Throwable;)V public fun d (Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt index f5350512a56..b0b756fa266 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt @@ -1,7 +1,7 @@ package io.sentry.android.timber -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.Integration import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel @@ -21,10 +21,10 @@ class SentryTimberIntegration( private lateinit var tree: SentryTimberTree private lateinit var logger: ILogger - override fun register(hub: IHub, options: SentryOptions) { + override fun register(scopes: IScopes, options: SentryOptions) { logger = options.logger - tree = SentryTimberTree(hub, minEventLevel, minBreadcrumbLevel) + tree = SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel) Timber.plant(tree) logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.") 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 f3a0f599a98..dddab751333 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 @@ -2,7 +2,7 @@ package io.sentry.android.timber import android.util.Log import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.protocol.Message @@ -13,7 +13,7 @@ import timber.log.Timber */ @Suppress("TooManyFunctions") // we have to override all methods to be able to tweak logging class SentryTimberTree( - private val hub: IHub, + private val scopes: IScopes, private val minEventLevel: SentryLevel, private val minBreadcrumbLevel: SentryLevel ) : Timber.Tree() { @@ -269,7 +269,7 @@ class SentryTimberTree( logger = "Timber" } - hub.captureEvent(sentryEvent) + scopes.captureEvent(sentryEvent) } } @@ -296,7 +296,7 @@ class SentryTimberTree( else -> null } - breadCrumb?.let { hub.addBreadcrumb(it) } + breadCrumb?.let { scopes.addBreadcrumb(it) } } } diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt index a57853e0596..8bb85aa085c 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt @@ -1,6 +1,6 @@ package io.sentry.android.timber -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.protocol.SdkVersion @@ -16,7 +16,7 @@ import kotlin.test.assertTrue class SentryTimberIntegrationTest { private class Fixture { - val hub = mock() + val scopes = mock() val options = SentryOptions().apply { sdkVersion = SdkVersion("test", "1.2.3") } @@ -41,7 +41,7 @@ class SentryTimberIntegrationTest { @Test fun `Integrations plants a tree into Timber on register`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertEquals(1, Timber.treeCount()) @@ -53,16 +53,16 @@ class SentryTimberIntegrationTest { @Test fun `Integrations plants the SentryTimberTree tree`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) Timber.e(Throwable()) - verify(fixture.hub).captureEvent(any()) + verify(fixture.scopes).captureEvent(any()) } @Test fun `Integrations removes a tree from Timber on close integration`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertEquals(1, Timber.treeCount()) @@ -84,7 +84,7 @@ class SentryTimberIntegrationTest { minEventLevel = SentryLevel.INFO, minBreadcrumbLevel = SentryLevel.DEBUG ) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertEquals(sut.minEventLevel, SentryLevel.INFO) assertEquals(sut.minBreadcrumbLevel, SentryLevel.DEBUG) @@ -93,7 +93,7 @@ class SentryTimberIntegrationTest { @Test fun `Integration adds itself to the package list`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue( fixture.options.sdkVersion!!.packageSet.any { @@ -106,7 +106,7 @@ class SentryTimberIntegrationTest { @Test fun `Integration adds itself to the integration list`() { val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertTrue( fixture.options.sdkVersion!!.integrationSet.contains("Timber") diff --git a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt index 3d82b139ec1..2ab7ff64dbb 100644 --- a/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt +++ b/sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberTreeTest.kt @@ -1,7 +1,7 @@ package io.sentry.android.timber import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLevel import io.sentry.getExc import org.mockito.kotlin.any @@ -19,13 +19,13 @@ import kotlin.test.assertNull class SentryTimberTreeTest { private class Fixture { - val hub = mock() + val scopes = mock() fun getSut( minEventLevel: SentryLevel = SentryLevel.ERROR, minBreadcrumbLevel: SentryLevel = SentryLevel.INFO ): SentryTimberTree { - return SentryTimberTree(hub, minEventLevel, minBreadcrumbLevel) + return SentryTimberTree(scopes, minEventLevel, minBreadcrumbLevel) } } @@ -40,28 +40,28 @@ class SentryTimberTreeTest { fun `Tree captures an event if min level is equal`() { val sut = fixture.getSut() sut.e(Throwable()) - verify(fixture.hub).captureEvent(any()) + verify(fixture.scopes).captureEvent(any()) } @Test fun `Tree captures an event if min level is higher`() { val sut = fixture.getSut() sut.wtf(Throwable()) - verify(fixture.hub).captureEvent(any()) + verify(fixture.scopes).captureEvent(any()) } @Test fun `Tree won't capture an event if min level is lower`() { val sut = fixture.getSut() sut.d(Throwable()) - verify(fixture.hub, never()).captureEvent(any()) + verify(fixture.scopes, never()).captureEvent(any()) } @Test fun `Tree captures debug level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.d(Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.DEBUG, it.level) } @@ -72,7 +72,7 @@ class SentryTimberTreeTest { fun `Tree captures info level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.i(Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.INFO, it.level) } @@ -83,7 +83,7 @@ class SentryTimberTreeTest { fun `Tree captures warning level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.w(Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.WARNING, it.level) } @@ -94,7 +94,7 @@ class SentryTimberTreeTest { fun `Tree captures error level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.e(Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.ERROR, it.level) } @@ -105,7 +105,7 @@ class SentryTimberTreeTest { fun `Tree captures fatal level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.wtf(Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.FATAL, it.level) } @@ -116,7 +116,7 @@ class SentryTimberTreeTest { fun `Tree captures unknown as debug level event`() { val sut = fixture.getSut(SentryLevel.DEBUG) sut.log(15, Throwable()) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(SentryLevel.DEBUG, it.level) } @@ -128,7 +128,7 @@ class SentryTimberTreeTest { val sut = fixture.getSut() val throwable = Throwable() sut.e(throwable) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(throwable, it.getExc()) } @@ -139,7 +139,7 @@ class SentryTimberTreeTest { fun `Tree captures an event without an exception`() { val sut = fixture.getSut() sut.e("message") - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertNull(it.getExc()) } @@ -150,7 +150,7 @@ class SentryTimberTreeTest { fun `Tree captures an event and sets Timber as a logger`() { val sut = fixture.getSut() sut.e("message") - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals("Timber", it.logger) } @@ -164,7 +164,7 @@ class SentryTimberTreeTest { // only available thru static class Timber.tag("tag") Timber.e("message") - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals("tag", it.getTag("TimberTag")) } @@ -176,7 +176,7 @@ class SentryTimberTreeTest { val sut = fixture.getSut() Timber.plant(sut) Timber.e("message") - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertNull(it.getTag("TimberTag")) } @@ -187,7 +187,7 @@ class SentryTimberTreeTest { fun `Tree captures an event with given message`() { val sut = fixture.getSut() sut.e("message") - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertNotNull(it.message) { message -> assertEquals("message", message.message) @@ -200,7 +200,7 @@ class SentryTimberTreeTest { fun `Tree captures an event with formatted message and arguments, when provided`() { val sut = fixture.getSut() sut.e("test count: %d", 32) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertNotNull(it.message) { message -> assertEquals("test count: %d", message.message) @@ -216,7 +216,7 @@ class SentryTimberTreeTest { val sut = fixture.getSut() sut.e("test count: %d", 32) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("test count: 32", it.message) } @@ -227,28 +227,28 @@ class SentryTimberTreeTest { fun `Tree adds a breadcrumb if min level is equal`() { val sut = fixture.getSut() sut.i(Throwable("test")) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.scopes).addBreadcrumb(any()) } @Test fun `Tree adds a breadcrumb if min level is higher`() { val sut = fixture.getSut() sut.e(Throwable("test")) - verify(fixture.hub).addBreadcrumb(any()) + verify(fixture.scopes).addBreadcrumb(any()) } @Test fun `Tree won't add a breadcrumb if min level is lower`() { val sut = fixture.getSut(minBreadcrumbLevel = SentryLevel.ERROR) sut.i(Throwable("test")) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test fun `Tree adds an info breadcrumb`() { val sut = fixture.getSut() sut.i("message") - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("Timber", it.category) assertEquals(SentryLevel.INFO, it.level) @@ -261,7 +261,7 @@ class SentryTimberTreeTest { fun `Tree adds an error breadcrumb`() { val sut = fixture.getSut() sut.e(Throwable("test")) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("exception", it.category) assertEquals(SentryLevel.ERROR, it.level) @@ -274,7 +274,7 @@ class SentryTimberTreeTest { fun `Tree does not add a breadcrumb, if no message provided`() { val sut = fixture.getSut() sut.e(Throwable()) - verify(fixture.hub, never()).addBreadcrumb(any()) + verify(fixture.scopes, never()).addBreadcrumb(any()) } @Test diff --git a/sentry-android/build.gradle.kts b/sentry-android/build.gradle.kts index 81619b736f2..49f7e75006b 100644 --- a/sentry-android/build.gradle.kts +++ b/sentry-android/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { targetSdk = Config.Android.targetSdkVersion - minSdk = Config.Android.minSdkVersionNdk + minSdk = Config.Android.minSdkVersion } buildFeatures { diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index 1c80e1950b3..e106585156f 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -17,11 +17,11 @@ public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollogr public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String; public static final field SENTRY_APOLLO_3_VARIABLES Ljava/lang/String; public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V - public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;Z)V - public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;Z)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V + public synthetic fun (Lio/sentry/IScopes;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -40,12 +40,12 @@ public final class io/sentry/apollo3/SentryApollo3Interceptor : com/apollographq public final class io/sentry/apollo3/SentryApolloBuilderExtensionsKt { public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;)Lcom/apollographql/apollo3/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;)Lcom/apollographql/apollo3/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;Z)Lcom/apollographql/apollo3/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;ZLjava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder; - public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IScopes;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IScopes;Z)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder; public static final fun sentryTracing (Lcom/apollographql/apollo3/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;)Lcom/apollographql/apollo3/ApolloClient$Builder; - public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IHub;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; + public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;Lio/sentry/IScopes;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; public static synthetic fun sentryTracing$default (Lcom/apollographql/apollo3/ApolloClient$Builder;ZLjava/util/List;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ILjava/lang/Object;)Lcom/apollographql/apollo3/ApolloClient$Builder; } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 08cab179a54..abfa41e5e17 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -11,9 +11,9 @@ import com.apollographql.apollo3.network.http.HttpInterceptorChain import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan +import io.sentry.ScopesAdapter import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel @@ -31,6 +31,7 @@ import io.sentry.util.HttpUtils import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.Platform import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 @@ -41,7 +42,7 @@ import java.util.Locale private const val TRACE_ORIGIN = "auto.graphql.apollo3" class SentryApollo3HttpInterceptor @JvmOverloads constructor( - @ApiStatus.Internal private val hub: IHub = HubAdapter.getInstance(), + @ApiStatus.Internal private val scopes: IScopes = ScopesAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, private val captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) @@ -65,7 +66,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( request: HttpRequest, chain: HttpInterceptorChain ): HttpResponse { - val activeSpan = if (Platform.isAndroid()) hub.transaction else hub.span + val activeSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span val operationName = getHeader(HEADER_APOLLO_OPERATION_NAME, request.headers) val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE) @@ -77,7 +78,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( span = startChild(request, activeSpan, operationName, operationType, operationId) } - val modifiedRequest = maybeAddTracingHeaders(hub, request, span) + val modifiedRequest = maybeAddTracingHeaders(scopes, request, span) var httpResponse: HttpResponse? = null var statusCode: Int? = null @@ -117,14 +118,16 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( } } - private fun maybeAddTracingHeaders(hub: IHub, request: HttpRequest, span: ISpan?): HttpRequest { + private fun maybeAddTracingHeaders(scopes: IScopes, request: HttpRequest, span: ISpan?): HttpRequest { var cleanedHeaders = removeSentryInternalHeaders(request.headers).toMutableList() - TracingUtils.traceIfAllowed(hub, request.url, request.headers.filter { it.name == BaggageHeader.BAGGAGE_HEADER }.map { it.value }, span)?.let { - cleanedHeaders.add(HttpHeader(it.sentryTraceHeader.name, it.sentryTraceHeader.value)) - it.baggageHeader?.let { baggageHeader -> - cleanedHeaders = cleanedHeaders.filterNot { it.name == BaggageHeader.BAGGAGE_HEADER }.toMutableList().apply { - add(HttpHeader(baggageHeader.name, baggageHeader.value)) + if (!isIgnored()) { + TracingUtils.traceIfAllowed(scopes, request.url, request.headers.filter { it.name == BaggageHeader.BAGGAGE_HEADER }.map { it.value }, span)?.let { + cleanedHeaders.add(HttpHeader(it.sentryTraceHeader.name, it.sentryTraceHeader.value)) + it.baggageHeader?.let { baggageHeader -> + cleanedHeaders = cleanedHeaders.filterNot { it.name == BaggageHeader.BAGGAGE_HEADER }.toMutableList().apply { + add(HttpHeader(baggageHeader.name, baggageHeader.value)) + } } } } @@ -136,6 +139,10 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( return requestBuilder.build() } + private fun isIgnored(): Boolean { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN) + } + private fun removeSentryInternalHeaders(headers: List): List { return headers.filterNot { it.name.equals(SENTRY_APOLLO_3_VARIABLES, true) || @@ -179,7 +186,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( try { String(Base64.decode(it, Base64.NO_WRAP)) } catch (e: Throwable) { - hub.options.logger.log( + scopes.options.logger.log( SentryLevel.ERROR, "Error decoding internal apolloHeader $headerName", e @@ -218,7 +225,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( span.spanContext.sampled = false } } catch (e: Throwable) { - hub.options.logger.log( + scopes.options.logger.log( SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e @@ -256,7 +263,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( hint.set(APOLLO_RESPONSE, httpResponse) } - hub.addBreadcrumb(breadcrumb, hint) + scopes.addBreadcrumb(breadcrumb, hint) } // Extensions @@ -273,7 +280,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( private fun getHeaders(headers: List): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII - if (!hub.options.isSendDefaultPii) { + if (!scopes.options.isSendDefaultPii) { return null } @@ -311,7 +318,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( val body = try { response.body?.peek()?.readUtf8() ?: "" } catch (e: Throwable) { - hub.options.logger.log( + scopes.options.logger.log( SentryLevel.ERROR, "Error reading the response body.", e @@ -368,7 +375,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( urlDetails.applyToRequest(this) // Cookie is only sent if isSendDefaultPii is enabled cookies = - if (hub.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null + if (scopes.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null method = request.method.name headers = getHeaders(request.headers) apiTarget = "graphql" @@ -382,7 +389,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( it.writeTo(buffer) data = buffer.readUtf8() } catch (e: Throwable) { - hub.options.logger.log( + scopes.options.logger.log( SentryLevel.ERROR, "Error reading the request body.", e @@ -396,7 +403,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( val sentryResponse = Response().apply { // Set-Cookie is only sent if isSendDefaultPii is enabled due to PII - cookies = if (hub.options.isSendDefaultPii) { + cookies = if (scopes.options.isSendDefaultPii) { getHeader( "Set-Cookie", response.headers @@ -419,9 +426,9 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( event.contexts.setResponse(sentryResponse) event.fingerprints = fingerprints - hub.captureEvent(event, hint) + scopes.captureEvent(event, hint) } catch (e: Throwable) { - hub.options.logger.log( + scopes.options.logger.log( SentryLevel.ERROR, "Error capturing the GraphQL error.", e diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt index 2cdbc148fb6..b40b1c183d7 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApolloBuilderExtensions.kt @@ -1,14 +1,14 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes +import io.sentry.ScopesAdapter import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.apollo3.SentryApollo3HttpInterceptor.Companion.DEFAULT_CAPTURE_FAILED_REQUESTS @JvmOverloads fun ApolloClient.Builder.sentryTracing( - hub: IHub = HubAdapter.getInstance(), + scopes: IScopes = ScopesAdapter.getInstance(), captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS), beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null @@ -16,7 +16,7 @@ fun ApolloClient.Builder.sentryTracing( addInterceptor(SentryApollo3Interceptor()) addHttpInterceptor( SentryApollo3HttpInterceptor( - hub = hub, + scopes = scopes, captureFailedRequests = captureFailedRequests, failedRequestTargets = failedRequestTargets, beforeSpan = beforeSpan @@ -31,7 +31,7 @@ fun ApolloClient.Builder.sentryTracing( beforeSpan: SentryApollo3HttpInterceptor.BeforeSpanCallback? = null ): ApolloClient.Builder { return sentryTracing( - hub = HubAdapter.getInstance(), + scopes = ScopesAdapter.getInstance(), captureFailedRequests = captureFailedRequests, failedRequestTargets = failedRequestTargets, beforeSpan = beforeSpan diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt index 40406b77b58..b3f8b6d57e0 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt @@ -5,7 +5,7 @@ import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse import com.apollographql.apollo3.exception.ApolloException import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS @@ -35,7 +35,7 @@ import kotlin.test.assertTrue class SentryApollo3InterceptorClientErrors { class Fixture { val server = MockWebServer() - lateinit var hub: IHub + lateinit var scopes: IScopes private val responseBodyOk = """{ @@ -75,7 +75,7 @@ class SentryApollo3InterceptorClientErrors { ): ApolloClient { SentryIntegrationPackageStorage.getInstance().clearStorage() - hub = mock().apply { + scopes = mock().apply { whenever(options).thenReturn( SentryOptions().apply { dsn = "https://key@sentry.io/proj" @@ -84,7 +84,7 @@ class SentryApollo3InterceptorClientErrors { } ) } - whenever(hub.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) + whenever(scopes.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) val response = MockResponse() .setBody(responseBody) @@ -102,7 +102,7 @@ class SentryApollo3InterceptorClientErrors { val builder = ApolloClient.Builder() .serverUrl(server.url("?myQuery=query#myFragment").toString()) .sentryTracing( - hub = hub, + scopes = scopes, captureFailedRequests = captureFailedRequests, failedRequestTargets = failedRequestTargets ) @@ -123,7 +123,7 @@ class SentryApollo3InterceptorClientErrors { val sut = fixture.getSut(captureFailedRequests = false, responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -132,7 +132,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } // endregion @@ -165,7 +165,7 @@ class SentryApollo3InterceptorClientErrors { ) executeQuery(sut) - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -174,7 +174,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } // endregion @@ -187,7 +187,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val throwable = (it.throwableMechanism as ExceptionMechanismException) assertEquals("SentryApollo3Interceptor", throwable.exceptionMechanism.type) @@ -202,7 +202,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val throwable = (it.throwableMechanism as ExceptionMechanismException) assertEquals("GraphQL Request failed, name: LaunchDetails, type: query", throwable.throwable.message) @@ -217,7 +217,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val throwable = (it.throwableMechanism as ExceptionMechanismException) assertTrue(throwable.isSnapshot) @@ -238,7 +238,7 @@ class SentryApollo3InterceptorClientErrors { {"operationName":"LaunchDetails","variables":{"id":"83"},"query":"query LaunchDetails($escapeDolar: ID!) { launch(id: $escapeDolar) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }"} """.trimIndent() - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val request = it.request!! @@ -262,7 +262,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val request = it.request!! @@ -280,7 +280,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val response = it.contexts.response!! @@ -300,7 +300,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val response = it.contexts.response!! @@ -318,7 +318,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { assertEquals(listOf("LaunchDetails", "query", "200"), it.fingerprints) }, @@ -337,7 +337,7 @@ class SentryApollo3InterceptorClientErrors { executeQuery(sut) // HttpInterceptor does not throw for >= 400 - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test @@ -345,7 +345,7 @@ class SentryApollo3InterceptorClientErrors { val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) - whenever(fixture.hub.captureEvent(any(), any())).thenThrow(RuntimeException()) + whenever(fixture.scopes.captureEvent(any(), any())).thenThrow(RuntimeException()) executeQuery(sut) } @@ -360,7 +360,7 @@ class SentryApollo3InterceptorClientErrors { fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), check { val request = it.get(TypeCheckHint.APOLLO_REQUEST) diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 44d8bfd6243..03072ae8b5a 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -9,7 +9,7 @@ import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain import io.sentry.BaggageHeader import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.Scope import io.sentry.ScopeCallback @@ -24,6 +24,7 @@ import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback +import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.Apollo3PlatformTestManipulator @@ -40,6 +41,7 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -57,11 +59,11 @@ class SentryApollo3InterceptorTest { sdkVersion = SdkVersion("test", "1.2.3") } val scope = Scope(options) - val hub = mock().also { + val scopes = mock().also { whenever(it.options).thenReturn(options) doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) } - private var httpInterceptor = SentryApollo3HttpInterceptor(hub, captureFailedRequests = false) + private var httpInterceptor = SentryApollo3HttpInterceptor(scopes, captureFailedRequests = false) @SuppressWarnings("LongParameterList") fun getSut( @@ -93,7 +95,7 @@ class SentryApollo3InterceptorTest { ) if (beforeSpan != null) { - httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan, captureFailedRequests = false) + httpInterceptor = SentryApollo3HttpInterceptor(scopes, beforeSpan, captureFailedRequests = false) } val builder = ApolloClient.Builder() @@ -124,7 +126,7 @@ class SentryApollo3InterceptorTest { fun `creates a span around the successful request`() { executeQuery() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it, httpStatusCode = 200) assertEquals(SpanStatus.OK, it.spans.first().status) @@ -139,7 +141,7 @@ class SentryApollo3InterceptorTest { fun `creates a span around the failed request`() { executeQuery(fixture.getSut(httpStatusCode = 403)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it, httpStatusCode = 403) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) @@ -159,7 +161,7 @@ class SentryApollo3InterceptorTest { } executeQuery(fixture.getSut(interceptor = failingInterceptor)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it, httpStatusCode = 404, contentLength = null) assertEquals("POST", it.spans.first().data?.get(SpanDataConvention.HTTP_METHOD_KEY)) @@ -176,7 +178,7 @@ class SentryApollo3InterceptorTest { fun `creates a span around the request failing with network error`() { executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it, httpStatusCode = null, contentLength = null) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) @@ -192,7 +194,7 @@ class SentryApollo3InterceptorTest { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) executeQuery(isSpanActive = false) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -201,15 +203,25 @@ class SentryApollo3InterceptorTest { fun `when there is no active span, does not add sentry trace header to the request`() { executeQuery(isSpanActive = false) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + fixture.options.setIgnoredSpanOrigins(listOf("auto.graphql.apollo3")) + executeQuery(isSpanActive = false) + + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + @Test fun `when there is an active span, adds sentry trace headers to the request`() { executeQuery() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -217,7 +229,7 @@ class SentryApollo3InterceptorTest { @Test fun `when there is an active span, existing baggage headers are merged with sentry baggage into single header`() { executeQuery(sut = fixture.getSut(addThirdPartyBaggageHeader = true)) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) @@ -241,7 +253,7 @@ class SentryApollo3InterceptorTest { ) ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -261,7 +273,7 @@ class SentryApollo3InterceptorTest { ) ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(0, it.spans.size) }, @@ -281,7 +293,7 @@ class SentryApollo3InterceptorTest { ) ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) }, @@ -294,7 +306,7 @@ class SentryApollo3InterceptorTest { @Test fun `adds breadcrumb when http calls succeeds`() { executeQuery(fixture.getSut()) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) // response_body_size is added but mock webserver returns 0 always @@ -309,9 +321,9 @@ class SentryApollo3InterceptorTest { @Test fun `sets SDKVersion Info`() { - assertNotNull(fixture.hub.options.sdkVersion) - assert(fixture.hub.options.sdkVersion!!.integrationSet.contains("Apollo3")) - val packageInfo = fixture.hub.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-apollo-3" } + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("Apollo3")) + val packageInfo = fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-apollo-3" } assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } @@ -320,14 +332,14 @@ class SentryApollo3InterceptorTest { fun `attaches to root transaction on Android`() { Apollo3PlatformTestManipulator.pretendIsAndroid(true) executeQuery(fixture.getSut()) - verify(fixture.hub).transaction + verify(fixture.scopes).transaction } @Test fun `attaches to child span on non-Android`() { Apollo3PlatformTestManipulator.pretendIsAndroid(false) executeQuery(fixture.getSut()) - verify(fixture.hub).span + verify(fixture.scopes).span } private fun assertTransactionDetails(it: SentryTransaction, httpStatusCode: Int? = 200, contentLength: Long? = 0L) { @@ -350,9 +362,9 @@ class SentryApollo3InterceptorTest { private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking { var tx: ITransaction? = null if (isSpanActive) { - tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) - whenever(fixture.hub.transaction).thenReturn(tx) - whenever(fixture.hub.span).thenReturn(tx) + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.transaction).thenReturn(tx) + whenever(fixture.scopes.span).thenReturn(tx) } val coroutine = launch { diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt index 81775efc185..dc9a94f184d 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -3,7 +3,7 @@ package io.sentry.apollo3 import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.exception.ApolloException import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.SentryOptions import io.sentry.SentryTracer @@ -12,6 +12,7 @@ import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback +import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SentryTransaction import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -23,6 +24,7 @@ import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -32,7 +34,7 @@ class SentryApollo3InterceptorWithVariablesTest { class Fixture { val server = MockWebServer() - val hub = mock() + val scopes = mock() @SuppressWarnings("LongParameterList") fun getSut( @@ -54,7 +56,7 @@ class SentryApollo3InterceptorWithVariablesTest { socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, beforeSpan: BeforeSpanCallback? = null ): ApolloClient { - whenever(hub.options).thenReturn( + whenever(scopes.options).thenReturn( SentryOptions().apply { dsn = "http://key@localhost/proj" } @@ -68,7 +70,7 @@ class SentryApollo3InterceptorWithVariablesTest { ) return ApolloClient.Builder().serverUrl(server.url("/").toString()) - .sentryTracing(hub = hub, beforeSpan = beforeSpan, captureFailedRequests = false) + .sentryTracing(scopes = scopes, beforeSpan = beforeSpan, captureFailedRequests = false) .build() } } @@ -79,7 +81,7 @@ class SentryApollo3InterceptorWithVariablesTest { fun `creates a span around the successful request`() { executeQuery() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) @@ -94,7 +96,7 @@ class SentryApollo3InterceptorWithVariablesTest { fun `creates a span around the failed request`() { executeQuery(fixture.getSut(httpStatusCode = 403)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) @@ -109,7 +111,7 @@ class SentryApollo3InterceptorWithVariablesTest { fun `creates a span around the request failing with network error`() { executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) @@ -124,7 +126,7 @@ class SentryApollo3InterceptorWithVariablesTest { fun `handles non-ascii header values correctly`() { executeQuery(id = "á") - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) @@ -138,7 +140,7 @@ class SentryApollo3InterceptorWithVariablesTest { @Test fun `adds breadcrumb when http calls succeeds`() { executeQuery(fixture.getSut()) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) // response_body_size is added but mock webserver returns 0 always @@ -153,7 +155,7 @@ class SentryApollo3InterceptorWithVariablesTest { @Test fun `internal headers are not sent over the wire`() { executeQuery(fixture.getSut()) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_VARIABLES]) assertNull(recorderRequest.headers[SentryApollo3HttpInterceptor.SENTRY_APOLLO_3_OPERATION_TYPE]) } @@ -173,8 +175,8 @@ class SentryApollo3InterceptorWithVariablesTest { private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true, id: String = "83") = runBlocking { var tx: ITransaction? = null if (isSpanActive) { - tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) - whenever(fixture.hub.span).thenReturn(tx) + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.span).thenReturn(tx) } val coroutine = launch { diff --git a/sentry-apollo/api/sentry-apollo.api b/sentry-apollo/api/sentry-apollo.api index 8c18bce06eb..63eac6a1935 100644 --- a/sentry-apollo/api/sentry-apollo.api +++ b/sentry-apollo/api/sentry-apollo.api @@ -5,9 +5,9 @@ public final class io/sentry/apollo/BuildConfig { public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor { public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V + public synthetic fun (Lio/sentry/IScopes;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V public fun dispose ()V public fun interceptAsync (Lcom/apollographql/apollo/interceptor/ApolloInterceptor$InterceptorRequest;Lcom/apollographql/apollo/interceptor/ApolloInterceptorChain;Ljava/util/concurrent/Executor;Lcom/apollographql/apollo/interceptor/ApolloInterceptor$CallBack;)V 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 8191e48e4a4..a24507ce513 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -15,9 +15,9 @@ import com.apollographql.apollo.request.RequestHeaders import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan +import io.sentry.ScopesAdapter import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SpanDataConvention @@ -25,6 +25,7 @@ import io.sentry.SpanStatus import io.sentry.TypeCheckHint.APOLLO_REQUEST import io.sentry.TypeCheckHint.APOLLO_RESPONSE import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils import java.util.Locale import java.util.concurrent.Executor @@ -32,12 +33,12 @@ import java.util.concurrent.Executor private const val TRACE_ORIGIN = "auto.graphql.apollo" class SentryApolloInterceptor( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null ) : ApolloInterceptor { - constructor(hub: IHub) : this(hub, null) - constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) + constructor(scopes: IScopes) : this(scopes, null) + constructor(beforeSpan: BeforeSpanCallback) : this(ScopesAdapter.getInstance(), beforeSpan) init { addIntegrationToSdkVersion("Apollo") @@ -45,7 +46,7 @@ class SentryApolloInterceptor( } override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) { - val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span + val activeSpan = if (io.sentry.util.Platform.isAndroid()) scopes.transaction else scopes.span if (activeSpan == null) { val headers = addTracingHeaders(request, null) val modifiedRequest = request.toBuilder().requestHeaders(headers).build() @@ -115,10 +116,10 @@ class SentryApolloInterceptor( private fun addTracingHeaders(request: InterceptorRequest, span: ISpan?): RequestHeaders { val requestHeaderBuilder = request.requestHeaders.toBuilder() - if (hub.options.isTraceSampling) { + if (scopes.options.isTraceSampling && !isIgnored()) { // we have no access to URI, no way to verify tracing origins TracingUtils.trace( - hub, + scopes, listOf(request.requestHeaders.headerValue(BaggageHeader.BAGGAGE_HEADER)), span )?.let { tracingHeaders -> @@ -135,6 +136,10 @@ class SentryApolloInterceptor( return requestHeaderBuilder.build() } + private fun isIgnored(): Boolean { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN) + } + private fun startChild(request: InterceptorRequest, activeSpan: ISpan): ISpan { val operation = request.operation.name().name() val operationType = when (request.operation) { @@ -154,7 +159,7 @@ class SentryApolloInterceptor( try { newSpan = beforeSpan.execute(span, request, response) } catch (e: Exception) { - hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) + scopes.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) } } if (newSpan == null) { @@ -182,7 +187,7 @@ class SentryApolloInterceptor( set(APOLLO_REQUEST, httpRequest) set(APOLLO_RESPONSE, httpResponse) } - hub.addBreadcrumb(breadcrumb, hint) + scopes.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 d22c2fd3e58..937aae53404 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -5,7 +5,7 @@ import com.apollographql.apollo.coroutines.await import com.apollographql.apollo.exception.ApolloException import io.sentry.BaggageHeader import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ITransaction import io.sentry.Scope import io.sentry.ScopeCallback @@ -17,6 +17,7 @@ import io.sentry.SpanStatus import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction import io.sentry.util.ApolloPlatformTestManipulator @@ -33,6 +34,7 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -48,13 +50,13 @@ class SentryApolloInterceptorTest { sdkVersion = SdkVersion("test", "1.2.3") } val scope = Scope(options) - val hub = mock().also { + val scopes = mock().also { whenever(it.options).thenReturn(options) doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope( any() ) } - private var interceptor = SentryApolloInterceptor(hub) + private var interceptor = SentryApolloInterceptor(scopes) @SuppressWarnings("LongParameterList") fun getSut( @@ -84,7 +86,7 @@ class SentryApolloInterceptorTest { ) if (beforeSpan != null) { - interceptor = SentryApolloInterceptor(hub, beforeSpan) + interceptor = SentryApolloInterceptor(scopes, beforeSpan) } return ApolloClient.builder() .serverUrl(server.url("/")) @@ -104,7 +106,7 @@ class SentryApolloInterceptorTest { fun `creates a span around the successful request`() { executeQuery() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) @@ -120,7 +122,7 @@ class SentryApolloInterceptorTest { fun `creates a span around the failed request`() { executeQuery(fixture.getSut(httpStatusCode = 403)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) @@ -138,7 +140,7 @@ class SentryApolloInterceptorTest { fun `creates a span around the request failing with network error`() { executeQuery(fixture.getSut(socketPolicy = SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTransactionDetails(it) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) @@ -154,15 +156,25 @@ class SentryApolloInterceptorTest { fun `when there is no active span, adds sentry trace header to the request from scope`() { executeQuery(isSpanActive = false) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + fixture.options.setIgnoredSpanOrigins(listOf("auto.graphql.apollo")) + executeQuery(isSpanActive = false) + + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + @Test fun `when there is an active span, adds sentry trace headers to the request`() { executeQuery() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -176,7 +188,7 @@ class SentryApolloInterceptorTest { } ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -196,7 +208,7 @@ class SentryApolloInterceptorTest { } ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertTrue(it.spans.isEmpty()) }, @@ -212,7 +224,7 @@ class SentryApolloInterceptorTest { fixture.getSut { _, _, _ -> throw RuntimeException() } ) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) }, @@ -225,7 +237,7 @@ class SentryApolloInterceptorTest { @Test fun `adds breadcrumb when http calls succeeds`() { executeQuery() - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) assertEquals(280L, it.data["response_body_size"]) @@ -237,9 +249,9 @@ class SentryApolloInterceptorTest { @Test fun `sets SDKVersion Info`() { - assertNotNull(fixture.hub.options.sdkVersion) - assert(fixture.hub.options.sdkVersion!!.integrationSet.contains("Apollo")) - val packageInfo = fixture.hub.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-apollo" } + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("Apollo")) + val packageInfo = fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-apollo" } assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } @@ -248,14 +260,14 @@ class SentryApolloInterceptorTest { fun `attaches to root transaction on Android`() { ApolloPlatformTestManipulator.pretendIsAndroid(true) executeQuery(fixture.getSut()) - verify(fixture.hub).transaction + verify(fixture.scopes).transaction } @Test fun `attaches to child span on non-Android`() { ApolloPlatformTestManipulator.pretendIsAndroid(false) executeQuery(fixture.getSut()) - verify(fixture.hub).span + verify(fixture.scopes).span } private fun assertTransactionDetails(it: SentryTransaction) { @@ -273,9 +285,9 @@ class SentryApolloInterceptorTest { private fun executeQuery(sut: ApolloClient = fixture.getSut(), isSpanActive: Boolean = true) = runBlocking { var tx: ITransaction? = null if (isSpanActive) { - tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) - whenever(fixture.hub.transaction).thenReturn(tx) - whenever(fixture.hub.span).thenReturn(tx) + tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.scopes) + whenever(fixture.scopes.transaction).thenReturn(tx) + whenever(fixture.scopes.span).thenReturn(tx) } val coroutine = launch { diff --git a/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/gestures/ComposeGestureTargetLocator.java b/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/gestures/ComposeGestureTargetLocator.java index aaf085f4841..bd056aeb919 100644 --- a/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/gestures/ComposeGestureTargetLocator.java +++ b/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/gestures/ComposeGestureTargetLocator.java @@ -9,11 +9,13 @@ import androidx.compose.ui.semantics.SemanticsModifier; import androidx.compose.ui.semantics.SemanticsPropertyKey; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.compose.SentryComposeHelper; import io.sentry.compose.helper.BuildConfig; import io.sentry.internal.gestures.GestureTargetLocator; import io.sentry.internal.gestures.UiElement; +import io.sentry.util.AutoClosableReentrantLock; import java.lang.reflect.Field; import java.util.LinkedList; import java.util.List; @@ -29,6 +31,7 @@ public final class ComposeGestureTargetLocator implements GestureTargetLocator { private final @NotNull ILogger logger; private volatile @Nullable SentryComposeHelper composeHelper; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public ComposeGestureTargetLocator(final @NotNull ILogger logger) { this.logger = logger; @@ -43,7 +46,7 @@ public ComposeGestureTargetLocator(final @NotNull ILogger logger) { // lazy init composeHelper as it's using some reflection under the hood if (composeHelper == null) { - synchronized (this) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (composeHelper == null) { composeHelper = new SentryComposeHelper(logger); } diff --git a/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter.java b/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter.java index 81b843d2564..6568b495c35 100644 --- a/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter.java +++ b/sentry-compose-helper/src/jvmMain/java/io/sentry/compose/viewhierarchy/ComposeViewHierarchyExporter.java @@ -9,9 +9,11 @@ import androidx.compose.ui.semantics.SemanticsModifier; import androidx.compose.ui.semantics.SemanticsPropertyKey; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.compose.SentryComposeHelper; import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; import io.sentry.protocol.ViewHierarchyNode; +import io.sentry.util.AutoClosableReentrantLock; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -23,6 +25,7 @@ public final class ComposeViewHierarchyExporter implements ViewHierarchyExporter @NotNull private final ILogger logger; @Nullable private volatile SentryComposeHelper composeHelper; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); public ComposeViewHierarchyExporter(@NotNull final ILogger logger) { this.logger = logger; @@ -37,7 +40,7 @@ public boolean export(@NotNull final ViewHierarchyNode parent, @NotNull final Ob // lazy init composeHelper as it's using some reflection under the hood if (composeHelper == null) { - synchronized (this) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (composeHelper == null) { composeHelper = new SentryComposeHelper(logger); } diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 114c08a22ff..0253b972680 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -71,7 +71,7 @@ android { defaultConfig { targetSdk = Config.Android.targetSdkVersion - minSdk = Config.Android.minSdkVersionCompose + minSdk = Config.Android.minSdkVersion // for AGP 4.1 buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") diff --git a/sentry-graphql-22/api/sentry-graphql-22.api b/sentry-graphql-22/api/sentry-graphql-22.api new file mode 100644 index 00000000000..2bee0cf765d --- /dev/null +++ b/sentry-graphql-22/api/sentry-graphql-22.api @@ -0,0 +1,22 @@ +public final class io/sentry/graphql22/BuildConfig { + public static final field SENTRY_GRAPHQL22_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/graphql22/SentryInstrumentation : graphql/execution/instrumentation/SimplePerformantInstrumentation { + public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; + public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V + public fun (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V + public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; + public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/execution/instrumentation/InstrumentationContext; + public fun createState (Lgraphql/execution/instrumentation/parameters/InstrumentationCreateStateParameters;)Lgraphql/execution/instrumentation/InstrumentationState; + public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Lgraphql/schema/DataFetcher; + public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/execution/instrumentation/InstrumentationState;)Ljava/util/concurrent/CompletableFuture; +} + +public abstract interface class io/sentry/graphql22/SentryInstrumentation$BeforeSpanCallback : io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { +} + diff --git a/sentry-graphql-22/build.gradle.kts b/sentry-graphql-22/build.gradle.kts new file mode 100644 index 00000000000..5463456f8cc --- /dev/null +++ b/sentry-graphql-22/build.gradle.kts @@ -0,0 +1,89 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + api(projects.sentryGraphqlCore) + compileOnly(Config.Libs.graphQlJava22) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.Libs.springBootStarterGraphql) + testImplementation("com.netflix.graphql.dgs:graphql-error-types:4.9.2") + testImplementation(Config.Libs.graphQlJava22) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.graphql22") + buildConfigField("String", "SENTRY_GRAPHQL22_SDK_NAME", "\"${Config.Sentry.SENTRY_GRAPHQL22_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java b/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java new file mode 100644 index 00000000000..a62eeade700 --- /dev/null +++ b/sentry-graphql-22/src/main/java/io/sentry/graphql22/SentryInstrumentation.java @@ -0,0 +1,162 @@ +package io.sentry.graphql22; + +import graphql.ExecutionResult; +import graphql.execution.instrumentation.InstrumentationContext; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.schema.DataFetcher; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.ExceptionReporter; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql.SentrySubscriptionHandler; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +public final class SentryInstrumentation + extends graphql.execution.instrumentation.SimplePerformantInstrumentation { + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_EXCEPTIONS_CONTEXT_KEY} + */ + @Deprecated + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; + + private static final String TRACE_ORIGIN = "auto.graphql.graphql22"; + private final @NotNull SentryGraphqlInstrumentation instrumentation; + + /** + * @param beforeSpan callback when a span is created + * @param subscriptionHandler can report subscription errors + * @param captureRequestBodyForNonSubscriptions false if request bodies should not be captured by + * this integration for query and mutation operations. This can be used to prevent unnecessary + * work by not adding the request body when another integration will add it anyways, as is the + * case with our spring integration for WebMVC. + */ + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions) { + this( + beforeSpan, + subscriptionHandler, + new ExceptionReporter(captureRequestBodyForNonSubscriptions), + new ArrayList<>()); + } + + /** + * @param beforeSpan callback when a span is created + * @param subscriptionHandler can report subscription errors + * @param captureRequestBodyForNonSubscriptions false if request bodies should not be captured by + * this integration for query and mutation operations. This can be used to prevent unnecessary + * work by not adding the request body when another integration will add it anyways, as is the + * case with our spring integration for WebMVC. + * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry + */ + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes) { + this( + beforeSpan, + subscriptionHandler, + new ExceptionReporter(captureRequestBodyForNonSubscriptions), + ignoredErrorTypes); + } + + @TestOnly + public SentryInstrumentation( + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes) { + this.instrumentation = + new SentryGraphqlInstrumentation( + beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); + SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL-v22"); + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-graphql-22", BuildConfig.VERSION_NAME); + } + + /** + * @param subscriptionHandler can report subscription errors + * @param captureRequestBodyForNonSubscriptions false if request bodies should not be captured by + * this integration for query and mutation operations. This can be used to prevent unnecessary + * work by not adding the request body when another integration will add it anyways, as is the + * case with our spring integration for WebMVC. + */ + public SentryInstrumentation( + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions) { + this(null, subscriptionHandler, captureRequestBodyForNonSubscriptions); + } + + @Override + public @NotNull InstrumentationState createState( + final @NotNull InstrumentationCreateStateParameters parameters) { + return instrumentation.createState(); + } + + @Override + public @Nullable InstrumentationContext beginExecution( + final @NotNull InstrumentationExecutionParameters parameters, + final @NotNull InstrumentationState state) { + final SentryGraphqlInstrumentation.TracingState tracingState = + InstrumentationState.ofState(state); + instrumentation.beginExecution(parameters, tracingState); + return super.beginExecution(parameters, state); + } + + @Override + public @NotNull CompletableFuture instrumentExecutionResult( + final @NotNull ExecutionResult executionResult, + final @NotNull InstrumentationExecutionParameters parameters, + final @NotNull InstrumentationState state) { + return super.instrumentExecutionResult(executionResult, parameters, state) + .whenComplete( + (result, exception) -> { + instrumentation.instrumentExecutionResultComplete(parameters, result, exception); + }); + } + + @Override + public @Nullable InstrumentationContext beginExecuteOperation( + final @NotNull InstrumentationExecuteOperationParameters parameters, + final @NotNull InstrumentationState state) { + instrumentation.beginExecuteOperation(parameters); + return super.beginExecuteOperation(parameters, state); + } + + @Override + @SuppressWarnings({"FutureReturnValueIgnored"}) + public @NotNull DataFetcher instrumentDataFetcher( + final @NotNull DataFetcher dataFetcher, + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull InstrumentationState state) { + final SentryGraphqlInstrumentation.TracingState tracingState = + InstrumentationState.ofState(state); + return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); + } + + /** + * @deprecated please use {@link SentryGraphqlInstrumentation.BeforeSpanCallback} + */ + @Deprecated + @FunctionalInterface + public interface BeforeSpanCallback extends SentryGraphqlInstrumentation.BeforeSpanCallback {} +} diff --git a/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt new file mode 100644 index 00000000000..628001bfeb7 --- /dev/null +++ b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationAnotherTest.kt @@ -0,0 +1,379 @@ +package io.sentry.graphql22 + +import graphql.ErrorClassification +import graphql.ErrorType +import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.GraphQLContext +import graphql.GraphqlErrorException +import graphql.execution.ExecutionContext +import graphql.execution.ExecutionContextBuilder +import graphql.execution.ExecutionId +import graphql.execution.ExecutionStepInfo +import graphql.execution.ExecutionStrategyParameters +import graphql.execution.MergedField +import graphql.execution.MergedSelectionSet +import graphql.execution.ResultPath +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.Field +import graphql.language.OperationDefinition +import graphql.scalar.GraphqlStringCoercing +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.DataFetchingEnvironmentImpl +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.TransactionContext +import io.sentry.TypeCheckHint +import io.sentry.graphql.ExceptionReporter +import io.sentry.graphql.ExceptionReporter.ExceptionDetails +import io.sentry.graphql.SentryGraphqlInstrumentation +import io.sentry.graphql.SentrySubscriptionHandler +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertSame + +class SentryInstrumentationAnotherTest { + + class Fixture { + val scopes = mock() + lateinit var activeSpan: SentryTracer + lateinit var dataFetcher: DataFetcher + lateinit var fieldFetchParameters: InstrumentationFieldFetchParameters + lateinit var instrumentationExecutionParameters: InstrumentationExecutionParameters + lateinit var environment: DataFetchingEnvironment + lateinit var executionContext: ExecutionContext + lateinit var executionStrategyParameters: ExecutionStrategyParameters + lateinit var executionStepInfo: ExecutionStepInfo + lateinit var graphQLContext: GraphQLContext + lateinit var subscriptionHandler: SentrySubscriptionHandler + lateinit var exceptionReporter: ExceptionReporter + internal lateinit var instrumentationState: SentryGraphqlInstrumentation.TracingState + lateinit var instrumentationExecuteOperationParameters: InstrumentationExecuteOperationParameters + val query = """query greeting(name: "somename")""" + val variables = mapOf("variableA" to "value a") + + fun getSut(isTransactionActive: Boolean = true, operation: OperationDefinition.Operation = OperationDefinition.Operation.QUERY, graphQLContextParam: Map? = null, addTransactionToTracingState: Boolean = true, ignoredErrors: List = emptyList()): SentryInstrumentation { + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(activeSpan) + } else { + whenever(scopes.span).thenReturn(null) + } + + val defaultGraphQLContext = mapOf( + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to scopes + ) + val mergedField = + MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build() + exceptionReporter = mock() + subscriptionHandler = mock() + whenever(subscriptionHandler.onSubscriptionResult(any(), any(), any(), any())).thenReturn("result modified by subscription handler") + val instrumentation = SentryInstrumentation( + null, + subscriptionHandler, + exceptionReporter, + ignoredErrors + ) + dataFetcher = mock>() + whenever(dataFetcher.get(any())).thenReturn("raw result") + graphQLContext = GraphQLContext.newContext() + .of(graphQLContextParam ?: defaultGraphQLContext).build() + val scalarType = GraphQLScalarType.newScalar().name("MyResponseType").coercing( + GraphqlStringCoercing() + ).build() + val field = GraphQLFieldDefinition.newFieldDefinition() + .name("myQueryFieldName") + .type(scalarType) + .build() + val objectType = GraphQLObjectType.newObject().name("QUERY").field(field).build() + executionStepInfo = ExecutionStepInfo.newExecutionStepInfo() + .type(scalarType) + .fieldContainer(objectType) + .parentInfo(ExecutionStepInfo.newExecutionStepInfo().type(objectType).build()) + .path(ResultPath.rootPath().segment("child")) + .field(mergedField) + .build() + val operationDefinition = OperationDefinition.newOperationDefinition() + .operation(operation) + .name("operation name") + .build() + environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .graphQLContext(graphQLContext) + .executionStepInfo(executionStepInfo) + .operationDefinition(operationDefinition) + .build() + executionContext = ExecutionContextBuilder.newExecutionContextBuilder() + .executionId(ExecutionId.generate()) + .graphQLContext(graphQLContext) + .operationDefinition(operationDefinition) + .build() + executionStrategyParameters = ExecutionStrategyParameters.newParameters() + .executionStepInfo(executionStepInfo) + .fields(MergedSelectionSet.newMergedSelectionSet().build()) + .field(mergedField) + .build() + instrumentationState = SentryGraphqlInstrumentation.TracingState().also { + if (isTransactionActive && addTransactionToTracingState) { + it.transaction = activeSpan + } + } + fieldFetchParameters = InstrumentationFieldFetchParameters( + executionContext, + { environment }, + executionStrategyParameters, + false + ) + val executionInput = ExecutionInput.newExecutionInput() + .query(query) + .graphQLContext(graphQLContextParam ?: defaultGraphQLContext) + .variables(variables) + .build() + val schema = GraphQLSchema.newSchema().query( + GraphQLObjectType.newObject().name("QueryType").field( + field + ).build() + ).build() + instrumentationExecutionParameters = InstrumentationExecutionParameters(executionInput, schema) + instrumentationExecuteOperationParameters = InstrumentationExecuteOperationParameters(executionContext) + + return instrumentation + } + } + + private val fixture = Fixture() + + @Test + fun `invokes subscription handler for subscription`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.SUBSCRIPTION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("result modified by subscription handler", result) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + } + + @Test + fun `invokes subscription handler for subscription if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.SUBSCRIPTION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("result modified by subscription handler", result) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + } + + @Test + fun `does not invoke subscription handler for query`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.QUERY) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for query if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.QUERY) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for mutation`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.MUTATION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `does not invoke subscription handler for mutation if transaction is active`() { + val instrumentation = fixture.getSut(isTransactionActive = false, operation = OperationDefinition.Operation.MUTATION) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState) + val result = instrumentedDataFetcher.get(fixture.environment) + + assertEquals("raw result", result) + verify(fixture.subscriptionHandler, never()).onSubscriptionResult(any(), any(), any(), any()) + } + + @Test + fun `adds a breadcrumb for operation`() { + val instrumentation = fixture.getSut() + instrumentation.beginExecuteOperation(fixture.instrumentationExecuteOperationParameters, fixture.instrumentationState) + verify(fixture.scopes).addBreadcrumb( + org.mockito.kotlin.check { breadcrumb -> + assertEquals("graphql", breadcrumb.type) + assertEquals("query", breadcrumb.category) + assertEquals("operation name", breadcrumb.data["operation_name"]) + assertEquals("query", breadcrumb.data["operation_type"]) + assertEquals(fixture.executionContext.executionId.toString(), breadcrumb.data["operation_id"]) + } + ) + } + + @Test + fun `adds a breadcrumb for data fetcher`() { + val instrumentation = fixture.getSut() + instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters, fixture.instrumentationState).get(fixture.environment) + verify(fixture.scopes).addBreadcrumb( + org.mockito.kotlin.check { breadcrumb -> + assertEquals("graphql", breadcrumb.type) + assertEquals("graphql.fetcher", breadcrumb.category) + assertEquals("/child", breadcrumb.data["path"]) + assertEquals("myFieldName", breadcrumb.data["field"]) + assertEquals("MyResponseType", breadcrumb.data["type"]) + assertEquals("QUERY", breadcrumb.data["object_type"]) + }, + org.mockito.kotlin.check { hint -> + val environment = hint.getAs(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, DataFetchingEnvironment::class.java) + assertNotNull(environment) + } + ) + } + + @Test + fun `stores scopes in context and adds transaction to state`() { + val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.MUTATION, graphQLContextParam = emptyMap(), addTransactionToTracingState = false) + withMockScopes { + instrumentation.beginExecution(fixture.instrumentationExecutionParameters, fixture.instrumentationState) + assertSame(fixture.scopes, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY)) + assertNotNull(fixture.instrumentationState.transaction) + } + } + + @Test + fun `invokes exceptionReporter for error`() { + val instrumentation = fixture.getSut() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError( + GraphqlErrorException.newErrorException().message("exception message").errorClassification( + ErrorType.ValidationError + ).build() + ) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter).captureThrowable( + org.mockito.kotlin.check { + assertEquals("exception message", it.message) + }, + org.mockito.kotlin.check { + assertSame(fixture.scopes, it.scopes) + assertSame(fixture.query, it.query) + assertEquals(false, it.isSubscription) + assertEquals(fixture.variables, it.variables) + }, + same(executionResult) + ) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `invokes exceptionReporter for exceptions in GraphQLContext`() { + val exception = IllegalStateException("some exception") + val instrumentation = fixture.getSut( + graphQLContextParam = mapOf( + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to fixture.scopes + ) + ) + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter).captureThrowable( + org.mockito.kotlin.check { + assertSame(exception, it) + }, + org.mockito.kotlin.check { + assertSame(fixture.scopes, it.scopes) + assertSame(fixture.query, it.query) + assertEquals(false, it.isSubscription) + assertEquals(fixture.variables, it.variables) + }, + same(executionResult) + ) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `does not invoke exceptionReporter for certain errors that should be handled by SentryDataFetcherExceptionHandler`() { + val instrumentation = fixture.getSut() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(ErrorType.DataFetchingException).build()) + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(org.springframework.graphql.execution.ErrorType.INTERNAL_ERROR).build()) + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(com.netflix.graphql.types.errors.ErrorType.INTERNAL).build()) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any()) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `does not invoke exceptionReporter for ignored errors`() { + val instrumentation = fixture.getSut(ignoredErrors = listOf("SOME_ERROR")) + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .addError(GraphqlErrorException.newErrorException().message("exception message").errorClassification(SomeErrorClassification.SOME_ERROR).build()) + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any()) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + @Test + fun `never invokes exceptionReporter if no errors`() { + val instrumentation = fixture.getSut() + val executionResult = ExecutionResult.newExecutionResult() + .data("raw result") + .build() + val resultFuture = instrumentation.instrumentExecutionResult(executionResult, fixture.instrumentationExecutionParameters, fixture.instrumentationState) + verify(fixture.exceptionReporter, never()).captureThrowable(any(), any(), any()) + val result = resultFuture.get() + assertSame(executionResult, result) + } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } + + data class Show(val id: Int) + + enum class SomeErrorClassification : ErrorClassification { + SOME_ERROR; + } +} diff --git a/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt new file mode 100644 index 00000000000..bec8c209b18 --- /dev/null +++ b/sentry-graphql-22/src/test/kotlin/io/sentry/graphql22/SentryInstrumentationTest.kt @@ -0,0 +1,243 @@ +package io.sentry.graphql22 + +import graphql.GraphQL +import graphql.GraphQLContext +import graphql.execution.ExecutionContextBuilder +import graphql.execution.ExecutionId +import graphql.execution.ExecutionStepInfo +import graphql.execution.ExecutionStrategyParameters +import graphql.execution.MergedField +import graphql.execution.MergedSelectionSet +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.Field +import graphql.language.OperationDefinition +import graphql.scalar.GraphqlStringCoercing +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironmentImpl +import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import io.sentry.graphql.ExceptionReporter +import io.sentry.graphql.NoOpSubscriptionHandler +import io.sentry.graphql.SentryGraphqlInstrumentation +import io.sentry.graphql.SentrySubscriptionHandler +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.lang.RuntimeException +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SentryInstrumentationTest { + + class Fixture { + val scopes = mock() + lateinit var activeSpan: SentryTracer + + fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryGraphqlInstrumentation.BeforeSpanCallback? = null): GraphQL { + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) + val schema = """ + type Query { + shows: [Show] + } + + type Show { + id: Int + } + """.trimIndent() + + val graphQLSchema = SchemaGenerator().makeExecutableSchema(SchemaParser().parse(schema), buildRuntimeWiring(dataFetcherThrows)) + val graphQL = GraphQL.newGraphQL(graphQLSchema) + .instrumentation( + SentryInstrumentation( + beforeSpan, + NoOpSubscriptionHandler.getInstance(), + true + ) + ) + .build() + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(activeSpan) + } else { + whenever(scopes.span).thenReturn(null) + } + + return graphQL + } + + private fun buildRuntimeWiring(dataFetcherThrows: Boolean) = RuntimeWiring.newRuntimeWiring() + .type("Query") { + it.dataFetcher("shows") { + if (dataFetcherThrows) { + throw RuntimeException("error") + } else { + listOf(Show(Random.nextInt()), Show(Random.nextInt())) + } + } + }.build() + } + + private val fixture = Fixture() + + @Test + fun `when transaction is active, creates inner spans`() { + val sut = fixture.getSut() + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertEquals("auto.graphql.graphql22", span.spanContext.origin) + assertTrue(span.isFinished) + assertEquals(SpanStatus.OK, span.status) + } + } + + @Test + fun `when transaction is active, and data fetcher throws, creates inner spans`() { + val sut = fixture.getSut(dataFetcherThrows = true) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isNotEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertTrue(span.isFinished) + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + } + } + + @Test + fun `when transaction is not active, does not create spans`() { + val sut = fixture.getSut(isTransactionActive = false) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertTrue(fixture.activeSpan.children.isEmpty()) + } + } + + @Test + fun `beforeSpan can drop spans`() { + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { _, _, _ -> null }) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("Query.shows", span.description) + assertNotNull(span.isSampled) { + assertFalse(it) + } + } + } + + @Test + fun `beforeSpan can modify spans`() { + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) + + withMockScopes { + val result = sut.execute("{ shows { id } }") + + assertTrue(result.errors.isEmpty()) + assertEquals(1, fixture.activeSpan.children.size) + val span = fixture.activeSpan.children.first() + assertEquals("graphql", span.operation) + assertEquals("changed", span.description) + assertTrue(span.isFinished) + } + } + + @Test + fun `invokes subscription handler for subscription`() { + val exceptionReporter = mock() + val subscriptionHandler = mock() + whenever(subscriptionHandler.onSubscriptionResult(any(), any(), any(), any())).thenReturn("result modified by subscription handler") + val operation = OperationDefinition.Operation.SUBSCRIPTION + val instrumentation = SentryInstrumentation( + null, + subscriptionHandler, + exceptionReporter, + emptyList() + ) + val dataFetcher = mock>() + whenever(dataFetcher.get(any())).thenReturn("raw result") + val graphQLContext = GraphQLContext.newContext().build() + val executionStepInfo = ExecutionStepInfo.newExecutionStepInfo().type( + GraphQLScalarType.newScalar().name("MyResponseType").coercing( + GraphqlStringCoercing() + ).build() + ).build() + val environment = DataFetchingEnvironmentImpl.newDataFetchingEnvironment() + .graphQLContext(graphQLContext) + .executionStepInfo(executionStepInfo) + .operationDefinition(OperationDefinition.newOperationDefinition().operation(operation).build()) + .build() + val executionContext = ExecutionContextBuilder.newExecutionContextBuilder() + .executionId(ExecutionId.generate()) + .graphQLContext(graphQLContext) + .build() + val executionStrategyParameters = ExecutionStrategyParameters.newParameters() + .executionStepInfo(executionStepInfo) + .fields(MergedSelectionSet.newMergedSelectionSet().build()) + .field(MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build()) + .build() + val parameters = InstrumentationFieldFetchParameters( + executionContext, + { environment }, + executionStrategyParameters, + false + ) + val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters, SentryGraphqlInstrumentation.TracingState()) + val result = instrumentedDataFetcher.get(environment) + + assertNotNull(result) + assertEquals("result modified by subscription handler", result) + } + + @Test + fun `Integration adds itself to integration and package list`() { + withMockScopes { + val sut = fixture.getSut() + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("GraphQL-v22")) + val packageInfo = + fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-graphql-22" } + assertNotNull(packageInfo) + assert(packageInfo.version == BuildConfig.VERSION_NAME) + } + } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } + + data class Show(val id: Int) +} diff --git a/sentry-graphql-core/api/sentry-graphql-core.api b/sentry-graphql-core/api/sentry-graphql-core.api new file mode 100644 index 00000000000..7b63e2270d4 --- /dev/null +++ b/sentry-graphql-core/api/sentry-graphql-core.api @@ -0,0 +1,70 @@ +public final class io/sentry/graphql/BuildConfig { + public static final field SENTRY_GRAPHQL_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/graphql/ExceptionReporter { + public fun (Z)V + public fun captureThrowable (Ljava/lang/Throwable;Lio/sentry/graphql/ExceptionReporter$ExceptionDetails;Lgraphql/ExecutionResult;)V +} + +public final class io/sentry/graphql/ExceptionReporter$ExceptionDetails { + public fun (Lio/sentry/IScopes;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Z)V + public fun (Lio/sentry/IScopes;Lgraphql/schema/DataFetchingEnvironment;Z)V + public fun getHub ()Lio/sentry/IScopes; + public fun getQuery ()Ljava/lang/String; + public fun getScopes ()Lio/sentry/IScopes; + public fun getVariables ()Ljava/util/Map; + public fun isSubscription ()Z +} + +public final class io/sentry/graphql/GraphqlStringUtils { + public fun ()V + public static fun fieldToString (Lgraphql/execution/MergedField;)Ljava/lang/String; + public static fun objectTypeToString (Lgraphql/schema/GraphQLObjectType;)Ljava/lang/String; + public static fun typeToString (Lgraphql/schema/GraphQLOutputType;)Ljava/lang/String; +} + +public final class io/sentry/graphql/NoOpSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { + public static fun getInstance ()Lio/sentry/graphql/NoOpSubscriptionHandler; + public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + +public final class io/sentry/graphql/SentryGenericDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { + public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun (Lio/sentry/IScopes;Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; + public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; +} + +public final class io/sentry/graphql/SentryGraphqlExceptionHandler { + public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V + public fun handleException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; +} + +public final class io/sentry/graphql/SentryGraphqlInstrumentation { + public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; + public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;Ljava/lang/String;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;Ljava/lang/String;)V + public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)V + public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lio/sentry/graphql/SentryGraphqlInstrumentation$TracingState;)V + public fun createState ()Lgraphql/execution/instrumentation/InstrumentationState; + public fun instrumentDataFetcher (Lgraphql/schema/DataFetcher;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;Lio/sentry/graphql/SentryGraphqlInstrumentation$TracingState;)Lgraphql/schema/DataFetcher; + public fun instrumentExecutionResultComplete (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Lgraphql/ExecutionResult;Ljava/lang/Throwable;)V +} + +public abstract interface class io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan; +} + +public final class io/sentry/graphql/SentryGraphqlInstrumentation$TracingState : graphql/execution/instrumentation/InstrumentationState { + public fun ()V + public fun getTransaction ()Lio/sentry/ISpan; + public fun setTransaction (Lio/sentry/ISpan;)V +} + +public abstract interface class io/sentry/graphql/SentrySubscriptionHandler { + public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + diff --git a/sentry-graphql-core/build.gradle.kts b/sentry-graphql-core/build.gradle.kts new file mode 100644 index 00000000000..ed1c197acd1 --- /dev/null +++ b/sentry-graphql-core/build.gradle.kts @@ -0,0 +1,88 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + compileOnly(Config.Libs.graphQlJava) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.Libs.springBootStarterGraphql) + testImplementation("com.netflix.graphql.dgs:graphql-error-types:4.9.2") + testImplementation(Config.Libs.graphQlJava) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.graphql") + buildConfigField("String", "SENTRY_GRAPHQL_SDK_NAME", "\"${Config.Sentry.SENTRY_GRAPHQL_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java similarity index 82% rename from sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java index 30ccb214256..9bca0955e40 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/ExceptionReporter.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/ExceptionReporter.java @@ -5,7 +5,7 @@ import graphql.language.AstPrinter; import graphql.schema.DataFetchingEnvironment; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -33,7 +33,7 @@ public void captureThrowable( final @NotNull Throwable throwable, final @NotNull ExceptionDetails exceptionDetails, final @Nullable ExecutionResult result) { - final @NotNull IHub hub = exceptionDetails.getHub(); + final @NotNull IScopes scopes = exceptionDetails.getScopes(); final @NotNull Mechanism mechanism = new Mechanism(); mechanism.setType(MECHANISM_TYPE); mechanism.setHandled(false); @@ -43,44 +43,44 @@ public void captureThrowable( event.setLevel(SentryLevel.FATAL); final @NotNull Hint hint = new Hint(); - setRequestDetailsOnEvent(hub, exceptionDetails, event); + setRequestDetailsOnEvent(scopes, exceptionDetails, event); - if (result != null && isAllowedToAttachBody(hub)) { + if (result != null && isAllowedToAttachBody(scopes)) { final @NotNull Response response = new Response(); final @NotNull Map responseBody = result.toSpecification(); response.setData(responseBody); event.getContexts().setResponse(response); } - hub.captureEvent(event, hint); + scopes.captureEvent(event, hint); } - private boolean isAllowedToAttachBody(final @NotNull IHub hub) { - final @NotNull SentryOptions options = hub.getOptions(); + private boolean isAllowedToAttachBody(final @NotNull IScopes scopes) { + final @NotNull SentryOptions options = scopes.getOptions(); return options.isSendDefaultPii() && !SentryOptions.RequestSize.NONE.equals(options.getMaxRequestBodySize()); } private void setRequestDetailsOnEvent( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull ExceptionDetails exceptionDetails, final @NotNull SentryEvent event) { - hub.configureScope( + scopes.configureScope( (scope) -> { final @Nullable Request scopeRequest = scope.getRequest(); final @NotNull Request request = scopeRequest == null ? new Request() : scopeRequest; - setDetailsOnRequest(hub, exceptionDetails, request); + setDetailsOnRequest(scopes, exceptionDetails, request); event.setRequest(request); }); } private void setDetailsOnRequest( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull ExceptionDetails exceptionDetails, final @NotNull Request request) { request.setApiTarget("graphql"); - if (isAllowedToAttachBody(hub) + if (isAllowedToAttachBody(scopes) && (exceptionDetails.isSubscription() || captureRequestBodyForNonSubscriptions)) { final @NotNull Map data = new HashMap<>(); @@ -99,27 +99,27 @@ private void setDetailsOnRequest( public static final class ExceptionDetails { - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters; private final @Nullable DataFetchingEnvironment dataFetchingEnvironment; private final boolean isSubscription; public ExceptionDetails( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @Nullable InstrumentationExecutionParameters instrumentationExecutionParameters, final boolean isSubscription) { - this.hub = hub; + this.scopes = scopes; this.instrumentationExecutionParameters = instrumentationExecutionParameters; dataFetchingEnvironment = null; this.isSubscription = isSubscription; } public ExceptionDetails( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @Nullable DataFetchingEnvironment dataFetchingEnvironment, final boolean isSubscription) { - this.hub = hub; + this.scopes = scopes; this.dataFetchingEnvironment = dataFetchingEnvironment; instrumentationExecutionParameters = null; this.isSubscription = isSubscription; @@ -149,8 +149,16 @@ public boolean isSubscription() { return isSubscription; } - public @NotNull IHub getHub() { - return hub; + /** + * @deprecated please use {@link ExceptionDetails#getScopes()} instead. + */ + @Deprecated + public @NotNull IScopes getHub() { + return scopes; + } + + public @NotNull IScopes getScopes() { + return scopes; } } } diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/GraphqlStringUtils.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/GraphqlStringUtils.java similarity index 100% rename from sentry-graphql/src/main/java/io/sentry/graphql/GraphqlStringUtils.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/GraphqlStringUtils.java diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java similarity index 92% rename from sentry-graphql/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java index df241ce35b2..839f4137191 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/NoOpSubscriptionHandler.java @@ -1,7 +1,7 @@ package io.sentry.graphql; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import io.sentry.IHub; +import io.sentry.IScopes; import org.jetbrains.annotations.NotNull; public final class NoOpSubscriptionHandler implements SentrySubscriptionHandler { @@ -17,7 +17,7 @@ private NoOpSubscriptionHandler() {} @Override public @NotNull Object onSubscriptionResult( @NotNull Object result, - @NotNull IHub hub, + @NotNull IScopes scopes, @NotNull ExceptionReporter exceptionReporter, @NotNull InstrumentationFieldFetchParameters parameters) { return result; diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java similarity index 93% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java index 6251d00779a..1287d38caa8 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGenericDataFetcherExceptionHandler.java @@ -3,7 +3,7 @@ import graphql.execution.DataFetcherExceptionHandler; import graphql.execution.DataFetcherExceptionHandlerParameters; import graphql.execution.DataFetcherExceptionHandlerResult; -import io.sentry.IHub; +import io.sentry.IScopes; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import org.jetbrains.annotations.NotNull; @@ -17,7 +17,7 @@ public final class SentryGenericDataFetcherExceptionHandler implements DataFetch private final @NotNull SentryGraphqlExceptionHandler handler; public SentryGenericDataFetcherExceptionHandler( - final @Nullable IHub hub, final @NotNull DataFetcherExceptionHandler delegate) { + final @Nullable IScopes scopes, final @NotNull DataFetcherExceptionHandler delegate) { this.handler = new SentryGraphqlExceptionHandler(delegate); } diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java similarity index 81% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java index a1f94caccec..4b178c498fe 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlExceptionHandler.java @@ -1,12 +1,14 @@ package io.sentry.graphql; -import static io.sentry.graphql.SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; +import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; import graphql.GraphQLContext; import graphql.execution.DataFetcherExceptionHandler; import graphql.execution.DataFetcherExceptionHandlerParameters; import graphql.execution.DataFetcherExceptionHandlerResult; import graphql.schema.DataFetchingEnvironment; +import io.sentry.ISentryLifecycleToken; +import io.sentry.util.AutoClosableReentrantLock; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; @@ -17,7 +19,8 @@ @ApiStatus.Internal public final class SentryGraphqlExceptionHandler { private final @Nullable DataFetcherExceptionHandler delegate; - private final @NotNull Object exceptionContextLock = new Object(); + private final @NotNull AutoClosableReentrantLock exceptionContextLock = + new AutoClosableReentrantLock(); public SentryGraphqlExceptionHandler(final @Nullable DataFetcherExceptionHandler delegate) { this.delegate = delegate; @@ -30,7 +33,7 @@ public SentryGraphqlExceptionHandler(final @Nullable DataFetcherExceptionHandler if (environment != null) { final @Nullable GraphQLContext graphQlContext = environment.getGraphQlContext(); if (graphQlContext != null) { - synchronized (exceptionContextLock) { + try (final @NotNull ISentryLifecycleToken ignored = exceptionContextLock.acquire()) { final @NotNull List exceptions = graphQlContext.getOrDefault( SENTRY_EXCEPTIONS_CONTEXT_KEY, new CopyOnWriteArrayList()); diff --git a/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java new file mode 100644 index 00000000000..c316774c045 --- /dev/null +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentryGraphqlInstrumentation.java @@ -0,0 +1,341 @@ +package io.sentry.graphql; + +import graphql.ErrorClassification; +import graphql.ExecutionResult; +import graphql.GraphQLContext; +import graphql.GraphQLError; +import graphql.execution.ExecutionContext; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.instrumentation.InstrumentationState; +import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.language.OperationDefinition; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.NoOpScopes; +import io.sentry.Sentry; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TypeCheckHint; +import io.sentry.util.StringUtils; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +public final class SentryGraphqlInstrumentation { + + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = "sentry.scopes"; + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = "sentry.exceptions"; + + private static final @NotNull List ERROR_TYPES_HANDLED_BY_DATA_FETCHERS = + Arrays.asList( + "INTERNAL_ERROR", // spring-graphql + "INTERNAL", // Netflix DGS + "DataFetchingException" // raw graphql-java + ); + private final @Nullable BeforeSpanCallback beforeSpan; + private final @NotNull SentrySubscriptionHandler subscriptionHandler; + private final @NotNull ExceptionReporter exceptionReporter; + private final @NotNull List ignoredErrorTypes; + private final @NotNull String traceOrigin; + + /** + * @param beforeSpan callback when a span is created + * @param subscriptionHandler can report subscription errors + * @param captureRequestBodyForNonSubscriptions false if request bodies should not be captured by + * this integration for query and mutation operations. This can be used to prevent unnecessary + * work by not adding the request body when another integration will add it anyways, as is the + * case with our spring integration for WebMVC. + * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry + */ + public SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final boolean captureRequestBodyForNonSubscriptions, + final @NotNull List ignoredErrorTypes, + final @NotNull String traceOrigin) { + this( + beforeSpan, + subscriptionHandler, + new ExceptionReporter(captureRequestBodyForNonSubscriptions), + ignoredErrorTypes, + traceOrigin); + } + + @TestOnly + public SentryGraphqlInstrumentation( + final @Nullable BeforeSpanCallback beforeSpan, + final @NotNull SentrySubscriptionHandler subscriptionHandler, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull List ignoredErrorTypes, + final @NotNull String traceOrigin) { + this.beforeSpan = beforeSpan; + this.subscriptionHandler = subscriptionHandler; + this.exceptionReporter = exceptionReporter; + this.ignoredErrorTypes = ignoredErrorTypes; + this.traceOrigin = traceOrigin; + } + + public @NotNull InstrumentationState createState() { + return new TracingState(); + } + + public void beginExecution( + final @NotNull InstrumentationExecutionParameters parameters, + final @NotNull TracingState tracingState) { + final @NotNull IScopes currentScopes = Sentry.getCurrentScopes(); + tracingState.setTransaction(currentScopes.getSpan()); + parameters.getGraphQLContext().put(SENTRY_SCOPES_CONTEXT_KEY, currentScopes); + } + + public void instrumentExecutionResultComplete( + final @NotNull InstrumentationExecutionParameters parameters, + final @Nullable ExecutionResult result, + final @Nullable Throwable exception) { + if (result != null) { + final @Nullable GraphQLContext graphQLContext = parameters.getGraphQLContext(); + if (graphQLContext != null) { + final @NotNull List exceptions = + graphQLContext.getOrDefault( + SENTRY_EXCEPTIONS_CONTEXT_KEY, new CopyOnWriteArrayList()); + for (Throwable throwable : exceptions) { + exceptionReporter.captureThrowable( + throwable, + new ExceptionReporter.ExceptionDetails( + scopesFromContext(graphQLContext), parameters, false), + result); + } + } + final @NotNull List errors = result.getErrors(); + if (errors != null) { + for (GraphQLError error : errors) { + String errorType = getErrorType(error); + if (!isIgnored(errorType)) { + exceptionReporter.captureThrowable( + new RuntimeException(error.getMessage()), + new ExceptionReporter.ExceptionDetails( + scopesFromContext(graphQLContext), parameters, false), + result); + } + } + } + } + if (exception != null) { + exceptionReporter.captureThrowable( + exception, + new ExceptionReporter.ExceptionDetails( + scopesFromContext(parameters.getGraphQLContext()), parameters, false), + null); + } + } + + private boolean isIgnored(final @Nullable String errorType) { + if (errorType == null) { + return false; + } + + // not capturing INTERNAL_ERRORS as they should be reported via graphQlContext above + // also not capturing error types explicitly ignored by users + return ERROR_TYPES_HANDLED_BY_DATA_FETCHERS.contains(errorType) + || ignoredErrorTypes.contains(errorType); + } + + private @Nullable String getErrorType(final @Nullable GraphQLError error) { + if (error == null) { + return null; + } + final @Nullable ErrorClassification errorType = error.getErrorType(); + if (errorType != null) { + return errorType.toString(); + } + final @Nullable Map extensions = error.getExtensions(); + if (extensions != null) { + return StringUtils.toString(extensions.get("errorType")); + } + return null; + } + + public void beginExecuteOperation( + final @NotNull InstrumentationExecuteOperationParameters parameters) { + final @Nullable ExecutionContext executionContext = parameters.getExecutionContext(); + if (executionContext != null) { + final @Nullable OperationDefinition operationDefinition = + executionContext.getOperationDefinition(); + if (operationDefinition != null) { + final @Nullable OperationDefinition.Operation operation = + operationDefinition.getOperation(); + final @Nullable String operationType = + operation == null ? null : operation.name().toLowerCase(Locale.ROOT); + scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) + .addBreadcrumb( + Breadcrumb.graphqlOperation( + operationDefinition.getName(), + operationType, + StringUtils.toString(executionContext.getExecutionId()))); + } + } + } + + private @NotNull IScopes scopesFromContext(final @Nullable GraphQLContext context) { + if (context == null) { + return NoOpScopes.getInstance(); + } + return context.getOrDefault(SENTRY_SCOPES_CONTEXT_KEY, NoOpScopes.getInstance()); + } + + @SuppressWarnings({"FutureReturnValueIgnored", "deprecation"}) + public @NotNull DataFetcher instrumentDataFetcher( + final @NotNull DataFetcher dataFetcher, + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull TracingState tracingState) { + // We only care about user code + if (parameters.isTrivialDataFetcher()) { + return dataFetcher; + } + + return environment -> { + final @Nullable ExecutionStepInfo executionStepInfo = environment.getExecutionStepInfo(); + if (executionStepInfo != null) { + Hint hint = new Hint(); + hint.set(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, environment); + scopesFromContext(parameters.getExecutionContext().getGraphQLContext()) + .addBreadcrumb( + Breadcrumb.graphqlDataFetcher( + StringUtils.toString(executionStepInfo.getPath()), + GraphqlStringUtils.fieldToString(executionStepInfo.getField()), + GraphqlStringUtils.typeToString(executionStepInfo.getType()), + GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType())), + hint); + } + final ISpan transaction = tracingState.getTransaction(); + if (transaction != null) { + final ISpan span = createSpan(transaction, parameters); + try { + final @Nullable Object tmpResult = dataFetcher.get(environment); + final @Nullable Object result = + maybeCallSubscriptionHandler(parameters, environment, tmpResult); + if (result instanceof CompletableFuture) { + ((CompletableFuture) result) + .whenComplete( + (r, ex) -> { + if (ex != null) { + span.setThrowable(ex); + span.setStatus(SpanStatus.INTERNAL_ERROR); + } else { + span.setStatus(SpanStatus.OK); + } + finish(span, environment, r); + }); + } else { + span.setStatus(SpanStatus.OK); + finish(span, environment, result); + } + return result; + } catch (Throwable e) { + span.setThrowable(e); + span.setStatus(SpanStatus.INTERNAL_ERROR); + finish(span, environment); + throw e; + } + } else { + final Object result = dataFetcher.get(environment); + return maybeCallSubscriptionHandler(parameters, environment, result); + } + }; + } + + private @Nullable Object maybeCallSubscriptionHandler( + final @NotNull InstrumentationFieldFetchParameters parameters, + final @NotNull DataFetchingEnvironment environment, + final @Nullable Object tmpResult) { + if (tmpResult == null) { + return null; + } + + if (OperationDefinition.Operation.SUBSCRIPTION.equals( + environment.getOperationDefinition().getOperation())) { + return subscriptionHandler.onSubscriptionResult( + tmpResult, + scopesFromContext(environment.getGraphQlContext()), + exceptionReporter, + parameters); + } + + return tmpResult; + } + + private void finish( + final @NotNull ISpan span, + final @NotNull DataFetchingEnvironment environment, + final @Nullable Object result) { + if (beforeSpan != null) { + final ISpan newSpan = beforeSpan.execute(span, environment, result); + if (newSpan == null) { + // span is dropped + span.getSpanContext().setSampled(false); + } else { + newSpan.finish(); + } + } else { + span.finish(); + } + } + + private void finish( + final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) { + finish(span, environment, null); + } + + private @NotNull ISpan createSpan( + @NotNull ISpan transaction, @NotNull InstrumentationFieldFetchParameters parameters) { + final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType(); + GraphQLObjectType parent; + if (type instanceof GraphQLNonNull) { + parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType(); + } else { + parent = (GraphQLObjectType) type; + } + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(traceOrigin); + final @NotNull ISpan span = + transaction.startChild( + "graphql", + parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName(), + spanOptions); + + return span; + } + + public static final class TracingState implements InstrumentationState { + private @Nullable ISpan transaction; + + public @Nullable ISpan getTransaction() { + return transaction; + } + + public void setTransaction(final @Nullable ISpan transaction) { + this.transaction = transaction; + } + } + + @FunctionalInterface + public interface BeforeSpanCallback { + @Nullable + ISpan execute( + @NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result); + } +} diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java similarity index 87% rename from sentry-graphql/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java rename to sentry-graphql-core/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java index bfc962b5010..0a5538ce221 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java +++ b/sentry-graphql-core/src/main/java/io/sentry/graphql/SentrySubscriptionHandler.java @@ -1,14 +1,14 @@ package io.sentry.graphql; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import io.sentry.IHub; +import io.sentry.IScopes; import org.jetbrains.annotations.NotNull; public interface SentrySubscriptionHandler { @NotNull Object onSubscriptionResult( @NotNull Object result, - @NotNull IHub hub, + @NotNull IScopes scopes, @NotNull ExceptionReporter exceptionReporter, @NotNull InstrumentationFieldFetchParameters parameters); } diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt similarity index 87% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt index a2b2b0f1010..df561f7169d 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt @@ -12,8 +12,8 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import io.sentry.Hint -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -39,7 +39,7 @@ class ExceptionReporterTest { it.maxRequestBodySize = SentryOptions.RequestSize.ALWAYS } val exception = IllegalStateException("some exception") - val hub = mock() + val scopes = mock() lateinit var instrumentationExecutionParameters: InstrumentationExecutionParameters lateinit var executionResult: ExecutionResult lateinit var scope: IScope @@ -47,7 +47,7 @@ class ExceptionReporterTest { val variables = mapOf("variableA" to "value a") fun getSut(options: SentryOptions = defaultOptions, captureRequestBodyForNonSubscriptions: Boolean = true): ExceptionReporter { - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) scope = Scope(options) val exceptionReporter = ExceptionReporter(captureRequestBodyForNonSubscriptions) executionResult = ExecutionResultImpl.newExecutionResult() @@ -75,9 +75,9 @@ class ExceptionReporterTest { field ).build() ).build() - val instrumentationState = SentryInstrumentation.TracingState() + val instrumentationState = SentryGraphqlInstrumentation.TracingState() instrumentationExecutionParameters = InstrumentationExecutionParameters(executionInput, schema, instrumentationState) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) return exceptionReporter } @@ -88,9 +88,9 @@ class ExceptionReporterTest { @Test fun `captures throwable`() { val exceptionReporter = fixture.getSut() - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, false), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, false), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) @@ -112,9 +112,9 @@ class ExceptionReporterTest { val exceptionReporter = fixture.getSut() val headers = mapOf("some-header" to "some-header-value") fixture.scope.request = Request().also { it.headers = headers } - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, false), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, false), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) @@ -136,9 +136,9 @@ class ExceptionReporterTest { @Test fun `does not attach query or variables if spring`() { val exceptionReporter = fixture.getSut(captureRequestBodyForNonSubscriptions = false) - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, false), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, false), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) @@ -156,9 +156,9 @@ class ExceptionReporterTest { @Test fun `does not attach query or variables if no max body size is set`() { val exceptionReporter = fixture.getSut(SentryOptions().also { it.isSendDefaultPii = true }, false) - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, false), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, false), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) @@ -176,9 +176,9 @@ class ExceptionReporterTest { @Test fun `does not attach query or variables if sendDefaultPii is false`() { val exceptionReporter = fixture.getSut(SentryOptions().also { it.maxRequestBodySize = SentryOptions.RequestSize.ALWAYS }, false) - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, false), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, false), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) @@ -196,9 +196,9 @@ class ExceptionReporterTest { @Test fun `attaches query and variables if spring and subscription`() { val exceptionReporter = fixture.getSut(captureRequestBodyForNonSubscriptions = false) - exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.hub, fixture.instrumentationExecutionParameters, true), fixture.executionResult) + exceptionReporter.captureThrowable(fixture.exception, ExceptionReporter.ExceptionDetails(fixture.scopes, fixture.instrumentationExecutionParameters, true), fixture.executionResult) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( org.mockito.kotlin.check { val ex = it.throwableMechanism as ExceptionMechanismException assertFalse(ex.exceptionMechanism.isHandled!!) diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt similarity index 100% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/GraphqlStringUtilsTest.kt diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt similarity index 88% rename from sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt rename to sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt index 6d643baf017..ee8cf36d77a 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt +++ b/sentry-graphql-core/src/test/kotlin/io/sentry/graphql/SentryGenericDataFetcherExceptionHandlerTest.kt @@ -4,7 +4,7 @@ import graphql.GraphQLContext import graphql.execution.DataFetcherExceptionHandler import graphql.execution.DataFetcherExceptionHandlerParameters import graphql.schema.DataFetchingEnvironmentImpl -import io.sentry.IHub +import io.sentry.IScopes import org.mockito.kotlin.mock import org.mockito.kotlin.verify import kotlin.test.Test @@ -15,10 +15,10 @@ class SentryGenericDataFetcherExceptionHandlerTest { @Test fun `collects exception into GraphQLContext and invokes delegate`() { - val hub = mock() + val scopes = mock() val delegate = mock() val handler = SentryGenericDataFetcherExceptionHandler( - hub, + scopes, delegate ) @@ -32,7 +32,7 @@ class SentryGenericDataFetcherExceptionHandlerTest { ).build() handler.onException(parameters) - val exceptions: List = parameters.dataFetchingEnvironment.graphQlContext[SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY] + val exceptions: List = parameters.dataFetchingEnvironment.graphQlContext[SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY] assertNotNull(exceptions) assertEquals(1, exceptions.size) assertEquals(exception, exceptions.first()) diff --git a/sentry-graphql/api/sentry-graphql.api b/sentry-graphql/api/sentry-graphql.api index 57c253e23cf..2e5f81ae760 100644 --- a/sentry-graphql/api/sentry-graphql.api +++ b/sentry-graphql/api/sentry-graphql.api @@ -3,61 +3,12 @@ public final class io/sentry/graphql/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } -public final class io/sentry/graphql/ExceptionReporter { - public fun (Z)V - public fun captureThrowable (Ljava/lang/Throwable;Lio/sentry/graphql/ExceptionReporter$ExceptionDetails;Lgraphql/ExecutionResult;)V -} - -public final class io/sentry/graphql/ExceptionReporter$ExceptionDetails { - public fun (Lio/sentry/IHub;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;Z)V - public fun (Lio/sentry/IHub;Lgraphql/schema/DataFetchingEnvironment;Z)V - public fun getHub ()Lio/sentry/IHub; - public fun getQuery ()Ljava/lang/String; - public fun getVariables ()Ljava/util/Map; - public fun isSubscription ()Z -} - -public final class io/sentry/graphql/GraphqlStringUtils { - public fun ()V - public static fun fieldToString (Lgraphql/execution/MergedField;)Ljava/lang/String; - public static fun objectTypeToString (Lgraphql/schema/GraphQLObjectType;)Ljava/lang/String; - public static fun typeToString (Lgraphql/schema/GraphQLOutputType;)Ljava/lang/String; -} - -public final class io/sentry/graphql/NoOpSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { - public static fun getInstance ()Lio/sentry/graphql/NoOpSubscriptionHandler; - public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IHub;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; -} - -public final class io/sentry/graphql/SentryDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; - public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; -} - -public final class io/sentry/graphql/SentryGenericDataFetcherExceptionHandler : graphql/execution/DataFetcherExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun (Lio/sentry/IHub;Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; - public fun onException (Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Lgraphql/execution/DataFetcherExceptionHandlerResult; -} - -public final class io/sentry/graphql/SentryGraphqlExceptionHandler { - public fun (Lgraphql/execution/DataFetcherExceptionHandler;)V - public fun handleException (Ljava/lang/Throwable;Lgraphql/schema/DataFetchingEnvironment;Lgraphql/execution/DataFetcherExceptionHandlerParameters;)Ljava/util/concurrent/CompletableFuture; -} - public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/instrumentation/SimpleInstrumentation { public static final field SENTRY_EXCEPTIONS_CONTEXT_KEY Ljava/lang/String; - public static final field SENTRY_HUB_CONTEXT_KEY Ljava/lang/String; - public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V - public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;)V - public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V - public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V - public fun (Lio/sentry/graphql/SentryInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V + public static final field SENTRY_SCOPES_CONTEXT_KEY Ljava/lang/String; + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Lio/sentry/graphql/ExceptionReporter;Ljava/util/List;)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;Z)V + public fun (Lio/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback;Lio/sentry/graphql/SentrySubscriptionHandler;ZLjava/util/List;)V public fun (Lio/sentry/graphql/SentrySubscriptionHandler;Z)V public fun beginExecuteOperation (Lgraphql/execution/instrumentation/parameters/InstrumentationExecuteOperationParameters;)Lgraphql/execution/instrumentation/InstrumentationContext; public fun beginExecution (Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Lgraphql/execution/instrumentation/InstrumentationContext; @@ -66,11 +17,6 @@ public final class io/sentry/graphql/SentryInstrumentation : graphql/execution/i public fun instrumentExecutionResult (Lgraphql/ExecutionResult;Lgraphql/execution/instrumentation/parameters/InstrumentationExecutionParameters;)Ljava/util/concurrent/CompletableFuture; } -public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback { - public abstract fun execute (Lio/sentry/ISpan;Lgraphql/schema/DataFetchingEnvironment;Ljava/lang/Object;)Lio/sentry/ISpan; -} - -public abstract interface class io/sentry/graphql/SentrySubscriptionHandler { - public abstract fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IHub;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +public abstract interface class io/sentry/graphql/SentryInstrumentation$BeforeSpanCallback : io/sentry/graphql/SentryGraphqlInstrumentation$BeforeSpanCallback { } diff --git a/sentry-graphql/build.gradle.kts b/sentry-graphql/build.gradle.kts index ed1c197acd1..f0de17f2880 100644 --- a/sentry-graphql/build.gradle.kts +++ b/sentry-graphql/build.gradle.kts @@ -22,6 +22,7 @@ tasks.withType().configureEach { dependencies { api(projects.sentry) + api(projects.sentryGraphqlCore) compileOnly(Config.Libs.graphQlJava) compileOnly(Config.CompileOnly.nopen) diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java b/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java deleted file mode 100644 index c0467c00891..00000000000 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryDataFetcherExceptionHandler.java +++ /dev/null @@ -1,67 +0,0 @@ -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.SentryIntegrationPackageStorage; -import io.sentry.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Captures exceptions that occur during data fetching, passes them to Sentry and invokes a delegate - * exception handler. - * - * @deprecated please use {@link SentryGenericDataFetcherExceptionHandler} in combination with - * {@link SentryInstrumentation} instead for better error reporting. - */ -@Deprecated -public final class SentryDataFetcherExceptionHandler implements DataFetcherExceptionHandler { - private final @NotNull IHub hub; - private final @NotNull DataFetcherExceptionHandler delegate; - - public SentryDataFetcherExceptionHandler( - final @NotNull IHub hub, final @NotNull DataFetcherExceptionHandler delegate) { - this.hub = Objects.requireNonNull(hub, "hub is required"); - this.delegate = Objects.requireNonNull(delegate, "delegate is required"); - SentryIntegrationPackageStorage.getInstance().addIntegration("GrahQLLegacyExceptionHandler"); - } - - public SentryDataFetcherExceptionHandler(final @NotNull DataFetcherExceptionHandler delegate) { - this(HubAdapter.getInstance(), delegate); - } - - @Override - public CompletableFuture handleException( - DataFetcherExceptionHandlerParameters handlerParameters) { - final Hint hint = new Hint(); - hint.set(GRAPHQL_HANDLER_PARAMETERS, handlerParameters); - - hub.captureException(handlerParameters.getException(), hint); - return delegate.handleException(handlerParameters); - } - - @SuppressWarnings("deprecation") - public DataFetcherExceptionHandlerResult onException( - final @NotNull DataFetcherExceptionHandlerParameters handlerParameters) { - final @Nullable CompletableFuture futureResult = - handleException(handlerParameters); - - if (futureResult != null) { - try { - return futureResult.get(); - } catch (InterruptedException | ExecutionException e) { - return DataFetcherExceptionHandlerResult.newResult().build(); - } - } else { - return DataFetcherExceptionHandlerResult.newResult().build(); - } - } -} diff --git a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java index d2d62c99d84..db2982e0802 100644 --- a/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java +++ b/sentry-graphql/src/main/java/io/sentry/graphql/SentryInstrumentation.java @@ -1,37 +1,16 @@ package io.sentry.graphql; -import graphql.ErrorClassification; import graphql.ExecutionResult; -import graphql.GraphQLContext; -import graphql.GraphQLError; -import graphql.execution.ExecutionContext; -import graphql.execution.ExecutionStepInfo; import graphql.execution.instrumentation.InstrumentationContext; import graphql.execution.instrumentation.InstrumentationState; import graphql.execution.instrumentation.parameters.InstrumentationExecuteOperationParameters; import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; -import graphql.language.OperationDefinition; import graphql.schema.DataFetcher; -import graphql.schema.DataFetchingEnvironment; -import graphql.schema.GraphQLNonNull; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import io.sentry.Breadcrumb; -import io.sentry.IHub; -import io.sentry.ISpan; -import io.sentry.NoOpHub; -import io.sentry.Sentry; import io.sentry.SentryIntegrationPackageStorage; -import io.sentry.SpanStatus; -import io.sentry.util.StringUtils; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -40,58 +19,22 @@ public final class SentryInstrumentation extends graphql.execution.instrumentation.SimpleInstrumentation { - private static final @NotNull List ERROR_TYPES_HANDLED_BY_DATA_FETCHERS = - Arrays.asList( - "INTERNAL_ERROR", // spring-graphql - "INTERNAL", // Netflix DGS - "DataFetchingException" // raw graphql-java - ); - public static final @NotNull String SENTRY_HUB_CONTEXT_KEY = "sentry.hub"; - public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = "sentry.exceptions"; - private static final String TRACE_ORIGIN = "auto.graphql.graphql"; - private final @Nullable BeforeSpanCallback beforeSpan; - private final @NotNull SentrySubscriptionHandler subscriptionHandler; - - private final @NotNull ExceptionReporter exceptionReporter; - - private final @NotNull List ignoredErrorTypes; - - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation() { - this(null, NoOpSubscriptionHandler.getInstance(), true); - } - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_SCOPES_CONTEXT_KEY} */ @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation(final @Nullable IHub hub) { - this(null, NoOpSubscriptionHandler.getInstance(), true); - } + public static final @NotNull String SENTRY_SCOPES_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. + * @deprecated please use {@link SentryGraphqlInstrumentation#SENTRY_EXCEPTIONS_CONTEXT_KEY} */ @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation(final @Nullable BeforeSpanCallback beforeSpan) { - this(beforeSpan, NoOpSubscriptionHandler.getInstance(), true); - } + public static final @NotNull String SENTRY_EXCEPTIONS_CONTEXT_KEY = + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY; - /** - * @deprecated please use a constructor that takes a {@link SentrySubscriptionHandler} instead. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public SentryInstrumentation( - final @Nullable IHub hub, final @Nullable BeforeSpanCallback beforeSpan) { - this(beforeSpan, NoOpSubscriptionHandler.getInstance(), true); - } + private static final String TRACE_ORIGIN = "auto.graphql.graphql"; + private final @NotNull SentryGraphqlInstrumentation instrumentation; /** * @param beforeSpan callback when a span is created @@ -102,7 +45,7 @@ public SentryInstrumentation( * case with our spring integration for WebMVC. */ public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions) { this( @@ -122,7 +65,7 @@ public SentryInstrumentation( * @param ignoredErrorTypes list of error types that should not be captured and sent to Sentry */ public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final boolean captureRequestBodyForNonSubscriptions, final @NotNull List ignoredErrorTypes) { @@ -135,14 +78,13 @@ public SentryInstrumentation( @TestOnly public SentryInstrumentation( - final @Nullable BeforeSpanCallback beforeSpan, + final @Nullable SentryGraphqlInstrumentation.BeforeSpanCallback beforeSpan, final @NotNull SentrySubscriptionHandler subscriptionHandler, final @NotNull ExceptionReporter exceptionReporter, final @NotNull List ignoredErrorTypes) { - this.beforeSpan = beforeSpan; - this.subscriptionHandler = subscriptionHandler; - this.exceptionReporter = exceptionReporter; - this.ignoredErrorTypes = ignoredErrorTypes; + this.instrumentation = + new SentryGraphqlInstrumentation( + beforeSpan, subscriptionHandler, exceptionReporter, ignoredErrorTypes, TRACE_ORIGIN); SentryIntegrationPackageStorage.getInstance().addIntegration("GraphQL"); SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-graphql", BuildConfig.VERSION_NAME); @@ -162,261 +104,50 @@ public SentryInstrumentation( } @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationState createState() { - return new TracingState(); + return instrumentation.createState(); } @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationContext beginExecution( final @NotNull InstrumentationExecutionParameters parameters) { - final TracingState tracingState = parameters.getInstrumentationState(); - final @NotNull IHub currentHub = Sentry.getCurrentHub(); - tracingState.setTransaction(currentHub.getSpan()); - parameters.getGraphQLContext().put(SENTRY_HUB_CONTEXT_KEY, currentHub); + final SentryGraphqlInstrumentation.TracingState tracingState = + parameters.getInstrumentationState(); + instrumentation.beginExecution(parameters, tracingState); return super.beginExecution(parameters); } @Override - @SuppressWarnings("deprecation") public CompletableFuture instrumentExecutionResult( ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return super.instrumentExecutionResult(executionResult, parameters) .whenComplete( (result, exception) -> { - if (result != null) { - final @Nullable GraphQLContext graphQLContext = parameters.getGraphQLContext(); - if (graphQLContext != null) { - final @NotNull List exceptions = - graphQLContext.getOrDefault( - SENTRY_EXCEPTIONS_CONTEXT_KEY, new CopyOnWriteArrayList()); - for (Throwable throwable : exceptions) { - exceptionReporter.captureThrowable( - throwable, - new ExceptionReporter.ExceptionDetails( - hubFromContext(graphQLContext), parameters, false), - result); - } - } - final @NotNull List errors = result.getErrors(); - if (errors != null) { - for (GraphQLError error : errors) { - String errorType = getErrorType(error); - if (!isIgnored(errorType)) { - exceptionReporter.captureThrowable( - new RuntimeException(error.getMessage()), - new ExceptionReporter.ExceptionDetails( - hubFromContext(graphQLContext), parameters, false), - result); - } - } - } - } - if (exception != null) { - exceptionReporter.captureThrowable( - exception, - new ExceptionReporter.ExceptionDetails( - hubFromContext(parameters.getGraphQLContext()), parameters, false), - null); - } + instrumentation.instrumentExecutionResultComplete(parameters, result, exception); }); } - private boolean isIgnored(final @Nullable String errorType) { - if (errorType == null) { - return false; - } - - // not capturing INTERNAL_ERRORS as they should be reported via graphQlContext above - // also not capturing error types explicitly ignored by users - return ERROR_TYPES_HANDLED_BY_DATA_FETCHERS.contains(errorType) - || ignoredErrorTypes.contains(errorType); - } - - private @Nullable String getErrorType(final @Nullable GraphQLError error) { - if (error == null) { - return null; - } - final @Nullable ErrorClassification errorType = error.getErrorType(); - if (errorType != null) { - return errorType.toString(); - } - final @Nullable Map extensions = error.getExtensions(); - if (extensions != null) { - return StringUtils.toString(extensions.get("errorType")); - } - return null; - } - @Override - @SuppressWarnings("deprecation") public @NotNull InstrumentationContext beginExecuteOperation( final @NotNull InstrumentationExecuteOperationParameters parameters) { - final @Nullable ExecutionContext executionContext = parameters.getExecutionContext(); - if (executionContext != null) { - final @Nullable OperationDefinition operationDefinition = - executionContext.getOperationDefinition(); - if (operationDefinition != null) { - final @Nullable OperationDefinition.Operation operation = - operationDefinition.getOperation(); - final @Nullable String operationType = - operation == null ? null : operation.name().toLowerCase(Locale.ROOT); - hubFromContext(parameters.getExecutionContext().getGraphQLContext()) - .addBreadcrumb( - Breadcrumb.graphqlOperation( - operationDefinition.getName(), - operationType, - StringUtils.toString(executionContext.getExecutionId()))); - } - } + instrumentation.beginExecuteOperation(parameters); return super.beginExecuteOperation(parameters); } - private @NotNull IHub hubFromContext(final @Nullable GraphQLContext context) { - if (context == null) { - return NoOpHub.getInstance(); - } - return context.getOrDefault(SENTRY_HUB_CONTEXT_KEY, NoOpHub.getInstance()); - } - @Override @SuppressWarnings({"FutureReturnValueIgnored", "deprecation"}) public @NotNull DataFetcher instrumentDataFetcher( final @NotNull DataFetcher dataFetcher, final @NotNull InstrumentationFieldFetchParameters parameters) { - // We only care about user code - if (parameters.isTrivialDataFetcher()) { - return dataFetcher; - } - - return environment -> { - final @Nullable ExecutionStepInfo executionStepInfo = environment.getExecutionStepInfo(); - if (executionStepInfo != null) { - hubFromContext(parameters.getExecutionContext().getGraphQLContext()) - .addBreadcrumb( - Breadcrumb.graphqlDataFetcher( - StringUtils.toString(executionStepInfo.getPath()), - GraphqlStringUtils.fieldToString(executionStepInfo.getField()), - GraphqlStringUtils.typeToString(executionStepInfo.getType()), - GraphqlStringUtils.objectTypeToString(executionStepInfo.getObjectType()))); - } - final TracingState tracingState = parameters.getInstrumentationState(); - final ISpan transaction = tracingState.getTransaction(); - if (transaction != null) { - final ISpan span = createSpan(transaction, parameters); - try { - final @Nullable Object tmpResult = dataFetcher.get(environment); - final @Nullable Object result = - maybeCallSubscriptionHandler(parameters, environment, tmpResult); - if (result instanceof CompletableFuture) { - ((CompletableFuture) result) - .whenComplete( - (r, ex) -> { - if (ex != null) { - span.setThrowable(ex); - span.setStatus(SpanStatus.INTERNAL_ERROR); - } else { - span.setStatus(SpanStatus.OK); - } - finish(span, environment, r); - }); - } else { - span.setStatus(SpanStatus.OK); - finish(span, environment, result); - } - return result; - } catch (Throwable e) { - span.setThrowable(e); - span.setStatus(SpanStatus.INTERNAL_ERROR); - finish(span, environment); - throw e; - } - } else { - final Object result = dataFetcher.get(environment); - return maybeCallSubscriptionHandler(parameters, environment, result); - } - }; - } - - private @Nullable Object maybeCallSubscriptionHandler( - final @NotNull InstrumentationFieldFetchParameters parameters, - final @NotNull DataFetchingEnvironment environment, - final @Nullable Object tmpResult) { - if (tmpResult == null) { - return null; - } - - if (OperationDefinition.Operation.SUBSCRIPTION.equals( - environment.getOperationDefinition().getOperation())) { - return subscriptionHandler.onSubscriptionResult( - tmpResult, - hubFromContext(environment.getGraphQlContext()), - exceptionReporter, - parameters); - } - - return tmpResult; - } - - private void finish( - final @NotNull ISpan span, - final @NotNull DataFetchingEnvironment environment, - final @Nullable Object result) { - if (beforeSpan != null) { - final ISpan newSpan = beforeSpan.execute(span, environment, result); - if (newSpan == null) { - // span is dropped - span.getSpanContext().setSampled(false); - } else { - newSpan.finish(); - } - } else { - span.finish(); - } - } - - private void finish( - final @NotNull ISpan span, final @NotNull DataFetchingEnvironment environment) { - finish(span, environment, null); - } - - private @NotNull ISpan createSpan( - @NotNull ISpan transaction, @NotNull InstrumentationFieldFetchParameters parameters) { - final GraphQLOutputType type = parameters.getExecutionStepInfo().getParent().getType(); - GraphQLObjectType parent; - if (type instanceof GraphQLNonNull) { - parent = (GraphQLObjectType) ((GraphQLNonNull) type).getWrappedType(); - } else { - parent = (GraphQLObjectType) type; - } - - final @NotNull ISpan span = - transaction.startChild( - "graphql", - parent.getName() + "." + parameters.getExecutionStepInfo().getPath().getSegmentName()); - - span.getSpanContext().setOrigin(TRACE_ORIGIN); - - return span; - } - - static final class TracingState implements InstrumentationState { - private @Nullable ISpan transaction; - - public @Nullable ISpan getTransaction() { - return transaction; - } - - public void setTransaction(final @Nullable ISpan transaction) { - this.transaction = transaction; - } + final SentryGraphqlInstrumentation.TracingState tracingState = + parameters.getInstrumentationState(); + return instrumentation.instrumentDataFetcher(dataFetcher, parameters, tracingState); } + /** + * @deprecated please use {@link SentryGraphqlInstrumentation.BeforeSpanCallback} + */ + @Deprecated @FunctionalInterface - public interface BeforeSpanCallback { - @Nullable - ISpan execute( - @NotNull ISpan span, @NotNull DataFetchingEnvironment environment, @Nullable Object result); - } + public interface BeforeSpanCallback extends SentryGraphqlInstrumentation.BeforeSpanCallback {} } diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt deleted file mode 100644 index b571fa82183..00000000000 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryDataFetcherExceptionHandlerTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.sentry.graphql - -import graphql.execution.DataFetcherExceptionHandler -import graphql.execution.DataFetcherExceptionHandlerParameters -import io.sentry.Hint -import io.sentry.IHub -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import kotlin.test.Test - -class SentryDataFetcherExceptionHandlerTest { - - @Test - fun `passes exception to Sentry and invokes delegate`() { - val hub = mock() - val delegate = mock() - val handler = SentryDataFetcherExceptionHandler(hub, delegate) - - val exception = RuntimeException() - val parameters = DataFetcherExceptionHandlerParameters.newExceptionParameters().exception(exception).build() - handler.onException(parameters) - - verify(hub).captureException(eq(exception), anyOrNull()) - verify(delegate).handleException(parameters) - } -} diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt index e30bbc9415e..7324c59a797 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationAnotherTest.kt @@ -28,14 +28,14 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.Hint +import io.sentry.IScopes import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.TransactionContext +import io.sentry.TypeCheckHint import io.sentry.graphql.ExceptionReporter.ExceptionDetails -import io.sentry.graphql.SentryInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY -import io.sentry.graphql.SentryInstrumentation.TracingState import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.eq @@ -52,7 +52,7 @@ import kotlin.test.assertSame class SentryInstrumentationAnotherTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var activeSpan: SentryTracer lateinit var dataFetcher: DataFetcher lateinit var fieldFetchParameters: InstrumentationFieldFetchParameters @@ -64,23 +64,23 @@ class SentryInstrumentationAnotherTest { lateinit var graphQLContext: GraphQLContext lateinit var subscriptionHandler: SentrySubscriptionHandler lateinit var exceptionReporter: ExceptionReporter - internal lateinit var instrumentationState: TracingState + internal lateinit var instrumentationState: SentryGraphqlInstrumentation.TracingState lateinit var instrumentationExecuteOperationParameters: InstrumentationExecuteOperationParameters val query = """query greeting(name: "somename")""" val variables = mapOf("variableA" to "value a") fun getSut(isTransactionActive: Boolean = true, operation: OperationDefinition.Operation = OperationDefinition.Operation.QUERY, graphQLContextParam: Map? = null, addTransactionToTracingState: Boolean = true, ignoredErrors: List = emptyList()): SentryInstrumentation { - whenever(hub.options).thenReturn(SentryOptions()) - activeSpan = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) if (isTransactionActive) { - whenever(hub.span).thenReturn(activeSpan) + whenever(scopes.span).thenReturn(activeSpan) } else { - whenever(hub.span).thenReturn(null) + whenever(scopes.span).thenReturn(null) } val defaultGraphQLContext = mapOf( - SentryInstrumentation.SENTRY_HUB_CONTEXT_KEY to hub + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to scopes ) val mergedField = MergedField.newMergedField().addField(Field.newField("myFieldName").build()).build() @@ -126,7 +126,7 @@ class SentryInstrumentationAnotherTest { .fields(MergedSelectionSet.newMergedSelectionSet().build()) .field(mergedField) .build() - instrumentationState = SentryInstrumentation.TracingState().also { + instrumentationState = SentryGraphqlInstrumentation.TracingState().also { if (isTransactionActive && addTransactionToTracingState) { it.transaction = activeSpan } @@ -165,7 +165,7 @@ class SentryInstrumentationAnotherTest { val result = instrumentedDataFetcher.get(fixture.environment) assertEquals("result modified by subscription handler", result) - verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.hub), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) } @Test @@ -175,7 +175,7 @@ class SentryInstrumentationAnotherTest { val result = instrumentedDataFetcher.get(fixture.environment) assertEquals("result modified by subscription handler", result) - verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.hub), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) + verify(fixture.subscriptionHandler).onSubscriptionResult(eq("raw result"), same(fixture.scopes), same(fixture.exceptionReporter), same(fixture.fieldFetchParameters)) } @Test @@ -222,7 +222,7 @@ class SentryInstrumentationAnotherTest { fun `adds a breadcrumb for operation`() { val instrumentation = fixture.getSut() instrumentation.beginExecuteOperation(fixture.instrumentationExecuteOperationParameters) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( org.mockito.kotlin.check { breadcrumb -> assertEquals("graphql", breadcrumb.type) assertEquals("query", breadcrumb.category) @@ -237,7 +237,7 @@ class SentryInstrumentationAnotherTest { fun `adds a breadcrumb for data fetcher`() { val instrumentation = fixture.getSut() instrumentation.instrumentDataFetcher(fixture.dataFetcher, fixture.fieldFetchParameters).get(fixture.environment) - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( org.mockito.kotlin.check { breadcrumb -> assertEquals("graphql", breadcrumb.type) assertEquals("graphql.fetcher", breadcrumb.category) @@ -245,16 +245,20 @@ class SentryInstrumentationAnotherTest { assertEquals("myFieldName", breadcrumb.data["field"]) assertEquals("MyResponseType", breadcrumb.data["type"]) assertEquals("QUERY", breadcrumb.data["object_type"]) + }, + org.mockito.kotlin.check { hint -> + val environment = hint.getAs(TypeCheckHint.GRAPHQL_DATA_FETCHING_ENVIRONMENT, DataFetchingEnvironment::class.java) + assertNotNull(environment) } ) } @Test - fun `stores hub in context and adds transaction to state`() { + fun `stores scopes in context and adds transaction to state`() { val instrumentation = fixture.getSut(isTransactionActive = true, operation = OperationDefinition.Operation.MUTATION, graphQLContextParam = emptyMap(), addTransactionToTracingState = false) - withMockHub { + withMockScopes { instrumentation.beginExecution(fixture.instrumentationExecutionParameters) - assertSame(fixture.hub, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryInstrumentation.SENTRY_HUB_CONTEXT_KEY)) + assertSame(fixture.scopes, fixture.instrumentationExecutionParameters.graphQLContext.get(SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY)) assertNotNull(fixture.instrumentationState.transaction) } } @@ -276,7 +280,7 @@ class SentryInstrumentationAnotherTest { assertEquals("exception message", it.message) }, org.mockito.kotlin.check { - assertSame(fixture.hub, it.hub) + assertSame(fixture.scopes, it.scopes) assertSame(fixture.query, it.query) assertEquals(false, it.isSubscription) assertEquals(fixture.variables, it.variables) @@ -292,8 +296,8 @@ class SentryInstrumentationAnotherTest { val exception = IllegalStateException("some exception") val instrumentation = fixture.getSut( graphQLContextParam = mapOf( - SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), - SentryInstrumentation.SENTRY_HUB_CONTEXT_KEY to fixture.hub + SentryGraphqlInstrumentation.SENTRY_EXCEPTIONS_CONTEXT_KEY to listOf(exception), + SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY to fixture.scopes ) ) val executionResult = ExecutionResultImpl.newExecutionResult() @@ -305,7 +309,7 @@ class SentryInstrumentationAnotherTest { assertSame(exception, it) }, org.mockito.kotlin.check { - assertSame(fixture.hub, it.hub) + assertSame(fixture.scopes, it.scopes) assertSame(fixture.query, it.query) assertEquals(false, it.isSubscription) assertEquals(fixture.variables, it.variables) @@ -356,8 +360,8 @@ class SentryInstrumentationAnotherTest { assertSame(executionResult, result) } - fun withMockHub(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { - it.`when` { Sentry.getCurrentHub() }.thenReturn(fixture.hub) + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) closure.invoke() } diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt index 8a579e26876..c8d63a1e987 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/SentryInstrumentationTest.kt @@ -18,7 +18,7 @@ import graphql.schema.GraphQLScalarType import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTracer @@ -39,12 +39,12 @@ import kotlin.test.assertTrue class SentryInstrumentationTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var activeSpan: SentryTracer - fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryInstrumentation.BeforeSpanCallback? = null): GraphQL { - whenever(hub.options).thenReturn(SentryOptions()) - activeSpan = SentryTracer(TransactionContext("name", "op"), hub) + fun getSut(isTransactionActive: Boolean = true, dataFetcherThrows: Boolean = false, beforeSpan: SentryGraphqlInstrumentation.BeforeSpanCallback? = null): GraphQL { + whenever(scopes.options).thenReturn(SentryOptions()) + activeSpan = SentryTracer(TransactionContext("name", "op"), scopes) val schema = """ type Query { shows: [Show] @@ -61,9 +61,9 @@ class SentryInstrumentationTest { .build() if (isTransactionActive) { - whenever(hub.span).thenReturn(activeSpan) + whenever(scopes.span).thenReturn(activeSpan) } else { - whenever(hub.span).thenReturn(null) + whenever(scopes.span).thenReturn(null) } return graphQL @@ -87,7 +87,7 @@ class SentryInstrumentationTest { fun `when transaction is active, creates inner spans`() { val sut = fixture.getSut() - withMockHub { + withMockScopes { val result = sut.execute("{ shows { id } }") assertTrue(result.errors.isEmpty()) @@ -105,7 +105,7 @@ class SentryInstrumentationTest { fun `when transaction is active, and data fetcher throws, creates inner spans`() { val sut = fixture.getSut(dataFetcherThrows = true) - withMockHub { + withMockScopes { val result = sut.execute("{ shows { id } }") assertTrue(result.errors.isNotEmpty()) @@ -122,7 +122,7 @@ class SentryInstrumentationTest { fun `when transaction is not active, does not create spans`() { val sut = fixture.getSut(isTransactionActive = false) - withMockHub { + withMockScopes { val result = sut.execute("{ shows { id } }") assertTrue(result.errors.isEmpty()) @@ -132,9 +132,9 @@ class SentryInstrumentationTest { @Test fun `beforeSpan can drop spans`() { - val sut = fixture.getSut(beforeSpan = SentryInstrumentation.BeforeSpanCallback { _, _, _ -> null }) + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { _, _, _ -> null }) - withMockHub { + withMockScopes { val result = sut.execute("{ shows { id } }") assertTrue(result.errors.isEmpty()) @@ -150,9 +150,9 @@ class SentryInstrumentationTest { @Test fun `beforeSpan can modify spans`() { - val sut = fixture.getSut(beforeSpan = SentryInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) + val sut = fixture.getSut(beforeSpan = SentryGraphqlInstrumentation.BeforeSpanCallback { span, _, _ -> span.apply { description = "changed" } }) - withMockHub { + withMockScopes { val result = sut.execute("{ shows { id } }") assertTrue(result.errors.isEmpty()) @@ -198,7 +198,7 @@ class SentryInstrumentationTest { environment, executionStrategyParameters, false - ).withNewState(SentryInstrumentation.TracingState()) + ).withNewState(SentryGraphqlInstrumentation.TracingState()) val instrumentedDataFetcher = instrumentation.instrumentDataFetcher(dataFetcher, parameters) val result = instrumentedDataFetcher.get(environment) @@ -208,19 +208,19 @@ class SentryInstrumentationTest { @Test fun `Integration adds itself to integration and package list`() { - withMockHub { + withMockScopes { val sut = fixture.getSut() - assertNotNull(fixture.hub.options.sdkVersion) - assert(fixture.hub.options.sdkVersion!!.integrationSet.contains("GraphQL")) + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("GraphQL")) val packageInfo = - fixture.hub.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-graphql" } + fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-graphql" } assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } } - fun withMockHub(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { - it.`when` { Sentry.getCurrentHub() }.thenReturn(fixture.hub) + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) closure.invoke() } diff --git a/sentry-jdbc/api/sentry-jdbc.api b/sentry-jdbc/api/sentry-jdbc.api index cff0f37fd26..dba5791f805 100644 --- a/sentry-jdbc/api/sentry-jdbc.api +++ b/sentry-jdbc/api/sentry-jdbc.api @@ -15,8 +15,9 @@ public final class io/sentry/jdbc/DatabaseUtils$DatabaseDetails { } public class io/sentry/jdbc/SentryJdbcEventListener : com/p6spy/engine/event/SimpleJdbcEventListener { + protected final field databaseDetailsLock Lio/sentry/util/AutoClosableReentrantLock; public fun ()V - public fun (Lio/sentry/IHub;)V + public fun (Lio/sentry/IScopes;)V public fun onAfterAnyExecute (Lcom/p6spy/engine/common/StatementInformation;JLjava/sql/SQLException;)V public fun onBeforeAnyExecute (Lcom/p6spy/engine/common/StatementInformation;)V } diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java index 0346d2d0b98..8a57e1eecac 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java @@ -6,12 +6,15 @@ import com.jakewharton.nopen.annotation.Open; import com.p6spy.engine.common.StatementInformation; import com.p6spy.engine.event.SimpleJdbcEventListener; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.ISpan; +import io.sentry.ScopesAdapter; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.Span; +import io.sentry.SpanOptions; import io.sentry.SpanStatus; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.sql.SQLException; import org.jetbrains.annotations.NotNull; @@ -21,28 +24,30 @@ @Open public class SentryJdbcEventListener extends SimpleJdbcEventListener { private static final String TRACE_ORIGIN = "auto.db.jdbc"; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private static final @NotNull ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); private volatile @Nullable DatabaseUtils.DatabaseDetails cachedDatabaseDetails = null; - private final @NotNull Object databaseDetailsLock = new Object(); + protected final @NotNull AutoClosableReentrantLock databaseDetailsLock = + new AutoClosableReentrantLock(); - public SentryJdbcEventListener(final @NotNull IHub hub) { - this.hub = Objects.requireNonNull(hub, "hub is required"); + public SentryJdbcEventListener(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); addPackageAndIntegrationInfo(); } public SentryJdbcEventListener() { - this(HubAdapter.getInstance()); + this(ScopesAdapter.getInstance()); } @Override public void onBeforeAnyExecute(final @NotNull StatementInformation statementInformation) { - final ISpan parent = hub.getSpan(); + final ISpan parent = scopes.getSpan(); if (parent != null && !parent.isNoOp()) { - final ISpan span = parent.startChild("db.query", statementInformation.getSql()); + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final ISpan span = parent.startChild("db.query", statementInformation.getSql(), spanOptions); CURRENT_SPAN.set(span); - span.getSpanContext().setOrigin(TRACE_ORIGIN); } } @@ -90,7 +95,7 @@ private void applyDatabaseDetailsToSpan( private @NotNull DatabaseUtils.DatabaseDetails getOrComputeDatabaseDetails( final @NotNull StatementInformation statementInformation) { if (cachedDatabaseDetails == null) { - synchronized (databaseDetailsLock) { + try (final @NotNull ISentryLifecycleToken ignored = databaseDetailsLock.acquire()) { if (cachedDatabaseDetails == null) { cachedDatabaseDetails = DatabaseUtils.readFrom(statementInformation); } 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 78c5d4cf12f..00ce03de416 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt @@ -2,7 +2,7 @@ package io.sentry.jdbc import com.p6spy.engine.common.StatementInformation import com.p6spy.engine.spy.P6DataSource -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention.DB_NAME_KEY @@ -26,7 +26,7 @@ import kotlin.test.assertTrue class SentryJdbcEventListenerTest { class Fixture { - val hub = mock().apply { + val scopes = mock().apply { whenever(options).thenReturn( SentryOptions().apply { sdkVersion = SdkVersion("test", "1.2.3") @@ -37,9 +37,9 @@ class SentryJdbcEventListenerTest { val actualDataSource = JDBCDataSource() fun getSut(withRunningTransaction: Boolean = true, existingRow: Int? = null): DataSource { - tx = SentryTracer(TransactionContext("name", "op"), hub) + tx = SentryTracer(TransactionContext("name", "op"), scopes) if (withRunningTransaction) { - whenever(hub.span).thenReturn(tx) + whenever(scopes.span).thenReturn(tx) } actualDataSource.setURL("jdbc:hsqldb:mem:testdb") @@ -54,7 +54,7 @@ class SentryJdbcEventListenerTest { } } - val sentryQueryExecutionListener = SentryJdbcEventListener(hub) + val sentryQueryExecutionListener = SentryJdbcEventListener(scopes) val p6spyDataSource = P6DataSource(actualDataSource) p6spyDataSource.setJdbcEventListenerFactory { sentryQueryExecutionListener } return p6spyDataSource @@ -131,9 +131,9 @@ class SentryJdbcEventListenerTest { @Test fun `sets SDKVersion Info`() { val sut = fixture.getSut() - assertNotNull(fixture.hub.options.sdkVersion) - assert(fixture.hub.options.sdkVersion!!.integrationSet.contains("JDBC")) - val packageInfo = fixture.hub.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-jdbc" } + assertNotNull(fixture.scopes.options.sdkVersion) + assert(fixture.scopes.options.sdkVersion!!.integrationSet.contains("JDBC")) + val packageInfo = fixture.scopes.options.sdkVersion!!.packageSet.firstOrNull { pkg -> pkg.name == "maven:io.sentry:sentry-jdbc" } assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } 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 2ca775572be..d6afd514e63 100644 --- a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java +++ b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java @@ -6,7 +6,8 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.HubAdapter; +import io.sentry.InitPriority; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryIntegrationPackageStorage; @@ -69,11 +70,10 @@ public SentryHandler(final @NotNull SentryOptions options) { if (configureFromLogManager) { retrieveProperties(); } - if (!Sentry.isEnabled()) { - options.setEnableExternalConfiguration(true); - options.setSdkVersion(createSdkVersion(options)); - Sentry.init(options); - } + options.setEnableExternalConfiguration(true); + options.setInitPriority(InitPriority.LOWEST); + options.setSdkVersion(createSdkVersion(options)); + Sentry.init(options); addPackageAndIntegrationInfo(); } @@ -210,9 +210,9 @@ SentryEvent createEvent(final @NotNull LogRecord record) { mdcProperties = CollectionUtils.filterMapEntries(mdcProperties, entry -> entry.getValue() != null); if (!mdcProperties.isEmpty()) { - // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else - final List contextTags = HubAdapter.getInstance().getOptions().getContextTags(); + final List contextTags = ScopesAdapter.getInstance().getOptions().getContextTags(); if (!contextTags.isEmpty()) { for (final String contextTag : contextTags) { // if mdc tag is listed in SentryOptions, apply as event tag 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 eaef25f0703..5b7048884d3 100644 --- a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt +++ b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt @@ -1,9 +1,11 @@ package io.sentry.jul +import io.sentry.InitPriority import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.checkEvent +import io.sentry.test.initForTest import io.sentry.transport.ITransport import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock @@ -57,13 +59,14 @@ class SentryHandlerTest { } @Test - fun `does not initialize Sentry if Sentry is already enabled`() { + fun `does not initialize Sentry if Sentry is already enabled with higher prio`() { val transport = mock() - Sentry.init { + initForTest { it.dsn = "http://key@localhost/proj" it.environment = "manual-environment" it.setTransportFactory { _, _ -> transport } it.isEnableBackpressureHandling = false + it.initPriority = InitPriority.LOW } fixture = Fixture(transport = transport) fixture.logger.severe("testing environment field") diff --git a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api index d501240a3ab..7e3be67279f 100644 --- a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api +++ b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api @@ -1,16 +1,16 @@ public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CopyableThreadContextElement { public fun ()V - public fun (Lio/sentry/IHub;)V - public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;)V + public synthetic fun (Lio/sentry/IScopes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement; public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; public fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext; public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; - public fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Lio/sentry/IHub;)V + public fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Lio/sentry/IScopes;)V public synthetic fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V - public fun updateThreadContext (Lkotlin/coroutines/CoroutineContext;)Lio/sentry/IHub; + public fun updateThreadContext (Lkotlin/coroutines/CoroutineContext;)Lio/sentry/IScopes; public synthetic fun updateThreadContext (Lkotlin/coroutines/CoroutineContext;)Ljava/lang/Object; } diff --git a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt index 3cf22a20da3..a77281a033b 100644 --- a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt +++ b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt @@ -1,6 +1,6 @@ package io.sentry.kotlin -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Sentry import kotlinx.coroutines.CopyableThreadContextElement import kotlin.coroutines.AbstractCoroutineContextElement @@ -9,26 +9,28 @@ import kotlin.coroutines.CoroutineContext /** * Sentry context element for [CoroutineContext]. */ -public class SentryContext(private val hub: IHub = Sentry.getCurrentHub().clone()) : - CopyableThreadContextElement, AbstractCoroutineContextElement(Key) { +public class SentryContext(private val scopes: IScopes = Sentry.forkedCurrentScope("coroutine")) : + CopyableThreadContextElement, AbstractCoroutineContextElement(Key) { private companion object Key : CoroutineContext.Key - override fun copyForChild(): CopyableThreadContextElement { - return SentryContext(hub.clone()) + @SuppressWarnings("deprecation") + override fun copyForChild(): CopyableThreadContextElement { + return SentryContext(scopes.forkedCurrentScope("coroutine.child")) } + @SuppressWarnings("deprecation") override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext { - return overwritingElement[Key] ?: SentryContext(hub.clone()) + return overwritingElement[Key] ?: SentryContext(scopes.forkedCurrentScope("coroutine.child")) } - override fun updateThreadContext(context: CoroutineContext): IHub { - val oldState = Sentry.getCurrentHub() - Sentry.setCurrentHub(hub) + override fun updateThreadContext(context: CoroutineContext): IScopes { + val oldState = Sentry.getCurrentScopes() + Sentry.setCurrentScopes(scopes) return oldState } - override fun restoreThreadContext(context: CoroutineContext, oldState: IHub) { - Sentry.setCurrentHub(oldState) + override fun restoreThreadContext(context: CoroutineContext, oldState: IScopes) { + Sentry.setCurrentScopes(oldState) } } diff --git a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt index b54ceabc511..086879f7d15 100644 --- a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt +++ b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt @@ -1,6 +1,8 @@ package io.sentry.kotlin +import io.sentry.ScopeType import io.sentry.Sentry +import io.sentry.test.initForTest import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -15,9 +17,13 @@ import kotlin.test.assertNull class SentryContextTest { + // TODO [HSM] In global hub mode SentryContext behaves differently + // because Sentry.getCurrentScopes always returns rootScopes + // What's the desired behaviour? + @BeforeTest fun init() { - Sentry.init("https://key@sentry.io/123") + initForTest("https://key@sentry.io/123") } @AfterTest @@ -38,11 +44,40 @@ class SentryContextTest { Sentry.setTag("c2", "c2value") assertEquals("c2value", getTag("c2")) assertEquals("parentValue", getTag("parent")) - assertNull(getTag("c1")) + assertNotNull(getTag("c1")) } listOf(c1, c2).joinAll() - assertNull(getTag("c1")) - assertNull(getTag("c2")) + assertNotNull(getTag("parent")) + assertNotNull(getTag("c1")) + assertNotNull(getTag("c2")) + return@runBlocking + } + + @Test + fun testContextIsNotPassedByDefaultBetweenCoroutinesCurrentScope() = runBlocking { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("parent", "parentValue") + } + val c1 = launch(SentryContext()) { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c1", "c1value") + } + assertEquals("c1value", getTag("c1", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + } + val c2 = launch(SentryContext()) { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c2", "c2value") + } + assertEquals("c2value", getTag("c2", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c1", ScopeType.CURRENT)) + } + listOf(c1, c2).joinAll() + assertNotNull(getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c1", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) } @Test @@ -84,7 +119,7 @@ class SentryContextTest { } @Test - fun testContextIsClonedWhenPassedToChild() = runBlocking { + fun testContextIsClonedWhenPassedToChildCurrentScope() = runBlocking { Sentry.setTag("parent", "parentValue") launch(SentryContext()) { Sentry.setTag("c1", "c1value") @@ -102,10 +137,44 @@ class SentryContextTest { c2.join() assertNotNull(getTag("c1")) - assertNull(getTag("c2")) + assertNotNull(getTag("c2")) + }.join() + assertNotNull(getTag("parent")) + assertNotNull(getTag("c1")) + assertNotNull(getTag("c2")) + return@runBlocking + } + + @Test + fun testContextIsClonedWhenPassedToChild() = runBlocking { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("parent", "parentValue") } - assertNull(getTag("c1")) - assertNull(getTag("c2")) + launch(SentryContext()) { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c1", "c1value") + } + assertEquals("c1value", getTag("c1", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + + val c2 = launch() { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c2", "c2value") + } + assertEquals("c2value", getTag("c2", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNotNull(getTag("c1", ScopeType.CURRENT)) + } + + c2.join() + + assertNotNull(getTag("c1", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + }.join() + assertNotNull(getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c1", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) } @Test @@ -119,7 +188,7 @@ class SentryContextTest { val c2 = launch( SentryContext( - Sentry.getCurrentHub().clone().also { + Sentry.getCurrentScopes().forkedScopes("test").also { it.setTag("cloned", "clonedValue") } ) @@ -136,16 +205,60 @@ class SentryContextTest { assertNotNull(getTag("c1")) assertNull(getTag("c2")) assertNull(getTag("cloned")) - } - assertNull(getTag("c1")) + }.join() + + assertNotNull(getTag("c1")) assertNull(getTag("c2")) assertNull(getTag("cloned")) + return@runBlocking + } + + @Test + fun testExplicitlyPassedContextOverridesPropagatedContextCurrentScope() = runBlocking { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("parent", "parentValue") + } + launch(SentryContext()) { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c1", "c1value") + } + assertEquals("c1value", getTag("c1", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + + val c2 = launch( + SentryContext( + Sentry.getCurrentScopes().forkedCurrentScope("test").also { + it.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("cloned", "clonedValue") + } + } + ) + ) { + Sentry.configureScope(ScopeType.CURRENT) { scope -> + scope.setTag("c2", "c2value") + } + assertEquals("c2value", getTag("c2", ScopeType.CURRENT)) + assertEquals("parentValue", getTag("parent", ScopeType.CURRENT)) + assertNotNull(getTag("c1", ScopeType.CURRENT)) + assertNotNull(getTag("cloned", ScopeType.CURRENT)) + } + + c2.join() + + assertNotNull(getTag("c1", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + assertNull(getTag("cloned", ScopeType.CURRENT)) + } + assertNull(getTag("c1", ScopeType.CURRENT)) + assertNull(getTag("c2", ScopeType.CURRENT)) + assertNull(getTag("cloned", ScopeType.CURRENT)) } @Test fun `mergeForChild returns copy of initial context if Key not present`() { val initialContextElement = SentryContext( - Sentry.getCurrentHub().clone().also { + Sentry.getCurrentScopes().forkedScopes("test").also { it.setTag("cloned", "clonedValue") } ) @@ -158,7 +271,7 @@ class SentryContextTest { @Test fun `mergeForChild returns passed context`() { val initialContextElement = SentryContext( - Sentry.getCurrentHub().clone().also { + Sentry.getCurrentScopes().forkedScopes("test").also { it.setTag("cloned", "clonedValue") } ) @@ -167,9 +280,9 @@ class SentryContextTest { assertEquals(initialContextElement, mergedContextElement) } - private fun getTag(tag: String): String? { + private fun getTag(tag: String, scopeType: ScopeType = ScopeType.ISOLATION): String? { var value: String? = null - Sentry.configureScope { + Sentry.configureScope(scopeType) { value = it.tags[tag] } return value diff --git a/sentry-log4j2/api/sentry-log4j2.api b/sentry-log4j2/api/sentry-log4j2.api index 76aa3e823e1..b7fe8b32737 100644 --- a/sentry-log4j2/api/sentry-log4j2.api +++ b/sentry-log4j2/api/sentry-log4j2.api @@ -5,7 +5,7 @@ public final class io/sentry/log4j2/BuildConfig { public class io/sentry/log4j2/SentryAppender : org/apache/logging/log4j/core/appender/AbstractAppender { public static final field MECHANISM_TYPE Ljava/lang/String; - 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 (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/IScopes;[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;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender; protected fun createBreadcrumb (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/Breadcrumb; 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 4cf4ad4a866..35b9d694192 100644 --- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java +++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java @@ -7,9 +7,10 @@ import io.sentry.Breadcrumb; import io.sentry.DateUtils; import io.sentry.Hint; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ITransportFactory; +import io.sentry.InitPriority; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryIntegrationPackageStorage; @@ -50,7 +51,7 @@ public class SentryAppender extends AbstractAppender { private @NotNull Level minimumBreadcrumbLevel = Level.INFO; private @NotNull Level minimumEventLevel = Level.ERROR; private final @Nullable Boolean debug; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @Nullable List contextTags; public SentryAppender( @@ -61,7 +62,7 @@ public SentryAppender( final @Nullable Level minimumEventLevel, final @Nullable Boolean debug, final @Nullable ITransportFactory transportFactory, - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @Nullable String[] contextTags) { super(name, filter, null, true, null); this.dsn = dsn; @@ -73,7 +74,7 @@ public SentryAppender( } this.debug = debug; this.transportFactory = transportFactory; - this.hub = hub; + this.scopes = scopes; this.contextTags = contextTags != null ? Arrays.asList(contextTags) : null; } @@ -110,34 +111,33 @@ public SentryAppender( minimumEventLevel, debug, null, - HubAdapter.getInstance(), + ScopesAdapter.getInstance(), contextTags != null ? contextTags.split(",") : null); } @Override public void start() { - if (!Sentry.isEnabled()) { - try { - Sentry.init( - options -> { - options.setEnableExternalConfiguration(true); - options.setDsn(dsn); - if (debug != null) { - options.setDebug(debug); + try { + Sentry.init( + options -> { + options.setEnableExternalConfiguration(true); + options.setInitPriority(InitPriority.LOWEST); + options.setDsn(dsn); + if (debug != null) { + options.setDebug(debug); + } + options.setSentryClientName( + BuildConfig.SENTRY_LOG4J2_SDK_NAME + "/" + BuildConfig.VERSION_NAME); + options.setSdkVersion(createSdkVersion(options)); + if (contextTags != null) { + for (final String contextTag : contextTags) { + options.addContextTag(contextTag); } - options.setSentryClientName( - BuildConfig.SENTRY_LOG4J2_SDK_NAME + "/" + BuildConfig.VERSION_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) { - LOGGER.warn("Failed to init Sentry during appender initialization: " + e.getMessage()); - } + } + Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory); + }); + } catch (IllegalArgumentException e) { + LOGGER.warn("Failed to init Sentry during appender initialization: " + e.getMessage()); } addPackageAndIntegrationInfo(); super.start(); @@ -149,13 +149,13 @@ public void append(final @NotNull LogEvent eventObject) { final Hint hint = new Hint(); hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject); - hub.captureEvent(createEvent(eventObject), hint); + scopes.captureEvent(createEvent(eventObject), hint); } if (eventObject.getLevel().isMoreSpecificThan(minimumBreadcrumbLevel)) { final Hint hint = new Hint(); hint.set(LOG4J_LOG_EVENT, eventObject); - hub.addBreadcrumb(createBreadcrumb(eventObject), hint); + scopes.addBreadcrumb(createBreadcrumb(eventObject), hint); } } @@ -199,9 +199,9 @@ public void append(final @NotNull LogEvent eventObject) { CollectionUtils.filterMapEntries( loggingEvent.getContextData().toMap(), entry -> entry.getValue() != null); if (!contextData.isEmpty()) { - // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else - final List contextTags = hub.getOptions().getContextTags(); + final List contextTags = scopes.getOptions().getContextTags(); if (contextTags != null && !contextTags.isEmpty()) { for (final String contextTag : contextTags) { // if mdc tag is listed in SentryOptions, apply as event tag 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 a99096d3150..f19e62ba572 100644 --- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt +++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt @@ -1,10 +1,12 @@ package io.sentry.log4j2 -import io.sentry.HubAdapter import io.sentry.ITransportFactory +import io.sentry.InitPriority +import io.sentry.ScopesAdapter import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.checkEvent +import io.sentry.test.initForTest import io.sentry.transport.ITransport import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager @@ -49,7 +51,7 @@ class SentryAppenderTest { } loggerContext.start() val config: Configuration = loggerContext.configuration - val appender = SentryAppender("sentry", null, "http://key@localhost/proj", minimumBreadcrumbLevel, minimumEventLevel, debug, this.transportFactory, HubAdapter.getInstance(), contextTags?.toTypedArray()) + val appender = SentryAppender("sentry", null, "http://key@localhost/proj", minimumBreadcrumbLevel, minimumEventLevel, debug, this.transportFactory, ScopesAdapter.getInstance(), contextTags?.toTypedArray()) config.addAppender(appender) val ref = AppenderRef.createAppenderRef("sentry", null, null) @@ -78,15 +80,17 @@ class SentryAppenderTest { @BeforeTest fun `clear MDC`() { ThreadContext.clearAll() + Sentry.close() } @Test - fun `does not initialize Sentry if Sentry is already enabled`() { - Sentry.init { + fun `does not initialize Sentry if Sentry is already enabled with higher prio`() { + initForTest { it.dsn = "http://key@localhost/proj" it.environment = "manual-environment" it.setTransportFactory(fixture.transportFactory) it.isEnableBackpressureHandling = false + it.initPriority = InitPriority.LOW } val logger = fixture.getSut() logger.error("testing environment field") @@ -446,6 +450,6 @@ class SentryAppenderTest { @Test fun `sets the debug mode`() { fixture.getSut(debug = true) - assertTrue(HubAdapter.getInstance().options.isDebug) + assertTrue(ScopesAdapter.getInstance().options.isDebug) } } 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 d0be1081496..77ce05f47f3 100644 --- a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java +++ b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java @@ -12,8 +12,9 @@ import io.sentry.Breadcrumb; import io.sentry.DateUtils; import io.sentry.Hint; -import io.sentry.HubAdapter; import io.sentry.ITransportFactory; +import io.sentry.InitPriority; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryIntegrationPackageStorage; @@ -49,24 +50,22 @@ 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); - options.setSentryClientName( - BuildConfig.SENTRY_LOGBACK_SDK_NAME + "/" + BuildConfig.VERSION_NAME); - options.setSdkVersion(createSdkVersion(options)); - Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory); - try { - Sentry.init(options); - } catch (IllegalArgumentException e) { - addWarn("Failed to init Sentry during appender initialization: " + e.getMessage()); - } - } else { - options - .getLogger() - .log(SentryLevel.WARNING, "DSN is null. SentryAppender is not being initialized"); + if (options.getDsn() == null || !options.getDsn().endsWith("_IS_UNDEFINED")) { + options.setEnableExternalConfiguration(true); + options.setInitPriority(InitPriority.LOWEST); + options.setSentryClientName( + BuildConfig.SENTRY_LOGBACK_SDK_NAME + "/" + BuildConfig.VERSION_NAME); + options.setSdkVersion(createSdkVersion(options)); + Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory); + try { + Sentry.init(options); + } catch (IllegalArgumentException e) { + addWarn("Failed to init Sentry during appender initialization: " + e.getMessage()); } + } else if (!Sentry.isEnabled()) { + options + .getLogger() + .log(SentryLevel.WARNING, "DSN is null. SentryAppender is not being initialized"); } addPackageAndIntegrationInfo(); super.start(); @@ -134,9 +133,9 @@ protected void append(@NotNull ILoggingEvent eventObject) { CollectionUtils.filterMapEntries( loggingEvent.getMDCPropertyMap(), entry -> entry.getValue() != null); if (!mdcProperties.isEmpty()) { - // get tags from HubAdapter options to allow getting the correct tags if Sentry has been + // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else - final List contextTags = HubAdapter.getInstance().getOptions().getContextTags(); + final List contextTags = ScopesAdapter.getInstance().getOptions().getContextTags(); if (!contextTags.isEmpty()) { for (final String contextTag : contextTags) { // if mdc tag is listed in SentryOptions, apply as event tag 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 b7797cadc74..526220d333b 100644 --- a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt @@ -8,10 +8,12 @@ import ch.qos.logback.core.encoder.Encoder import ch.qos.logback.core.encoder.EncoderBase import ch.qos.logback.core.status.Status import io.sentry.ITransportFactory +import io.sentry.InitPriority import io.sentry.Sentry import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.checkEvent +import io.sentry.test.initForTest import io.sentry.transport.ITransport import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -35,17 +37,18 @@ 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, contextTags: List? = null, encoder: Encoder? = null, sendDefaultPii: Boolean = false) { + private class Fixture(dsn: String? = "http://key@localhost/proj", minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, contextTags: List? = null, encoder: Encoder? = null, sendDefaultPii: Boolean = false, options: SentryOptions = SentryOptions(), startLater: Boolean = false) { val logger: Logger = LoggerFactory.getLogger(SentryAppenderTest::class.java) val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext val transportFactory = mock() val transport = mock() val utcTimeZone: ZoneId = ZoneId.of("UTC") + val appender = SentryAppender() + var encoder: Encoder? = null init { whenever(this.transportFactory.create(any(), any())).thenReturn(transport) - val appender = SentryAppender() - val options = SentryOptions() + this.encoder = encoder options.dsn = dsn options.isSendDefaultPii = sendDefaultPii contextTags?.forEach { options.addContextTag(it) } @@ -59,6 +62,12 @@ class SentryAppenderTest { val rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME) rootLogger.level = Level.TRACE rootLogger.addAppender(appender) + if (!startLater) { + start() + } + } + + fun start() { appender.start() encoder?.start() loggerContext.start() @@ -77,22 +86,32 @@ class SentryAppenderTest { @BeforeTest fun `clear MDC`() { MDC.clear() + Sentry.close() } @Test - fun `does not initialize Sentry if Sentry is already enabled`() { - fixture = Fixture() - Sentry.init { + fun `does not initialize Sentry if Sentry is already enabled with higher prio`() { + fixture = Fixture( + startLater = true, + options = SentryOptions().also { + it.setTag("only-present-if-logger-init-was-run", "another-value") + } + ) + initForTest { it.dsn = "http://key@localhost/proj" it.environment = "manual-environment" it.setTransportFactory(fixture.transportFactory) + it.setTag("tag-from-first-init", "some-value") it.isEnableBackpressureHandling = false + it.initPriority = InitPriority.LOW } + fixture.start() + fixture.logger.error("testing environment field") verify(fixture.transport).send( checkEvent { event -> - assertEquals("manual-environment", event.environment) + assertNull(event.tags?.get("only-present-if-logger-init-was-run")) }, anyOrNull() ) diff --git a/sentry-okhttp/api/sentry-okhttp.api b/sentry-okhttp/api/sentry-okhttp.api index 3095659c88d..9cb875ff341 100644 --- a/sentry-okhttp/api/sentry-okhttp.api +++ b/sentry-okhttp/api/sentry-okhttp.api @@ -6,12 +6,12 @@ public final class io/sentry/okhttp/BuildConfig { public class io/sentry/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { public static final field Companion Lio/sentry/okhttp/SentryOkHttpEventListener$Companion; public fun ()V - public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;)V - public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Lio/sentry/IHub;Lokhttp3/EventListener;)V - public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/sentry/IScopes;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;Lokhttp3/EventListener$Factory;)V + public synthetic fun (Lio/sentry/IScopes;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;Lokhttp3/EventListener;)V + public synthetic fun (Lio/sentry/IScopes;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lokhttp3/EventListener$Factory;)V public fun (Lokhttp3/EventListener;)V public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V @@ -50,9 +50,9 @@ public final class io/sentry/okhttp/SentryOkHttpEventListener$Companion { public class io/sentry/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { public fun ()V - public fun (Lio/sentry/IHub;)V - public fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V - public synthetic fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V + public synthetic fun (Lio/sentry/IScopes;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index 6bceb81a195..137af279135 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -2,19 +2,11 @@ package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryDate -import io.sentry.SentryLevel import io.sentry.SpanDataConvention import io.sentry.TypeCheckHint -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.transport.CurrentDateProvider import io.sentry.util.Platform import io.sentry.util.UrlUtils @@ -22,22 +14,20 @@ import okhttp3.Request import okhttp3.Response import java.util.Locale import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean private const val PROTOCOL_KEY = "protocol" private const val ERROR_MESSAGE_KEY = "error_message" -private const val RESPONSE_BODY_TIMEOUT_MILLIS = 800L internal const val TRACE_ORIGIN = "auto.http.okhttp" @Suppress("TooManyFunctions") -internal class SentryOkHttpEvent(private val hub: IHub, private val request: Request) { - private val eventSpans: MutableMap = ConcurrentHashMap() +internal class SentryOkHttpEvent(private val scopes: IScopes, private val request: Request) { + private val eventDates: MutableMap = ConcurrentHashMap() private val breadcrumb: Breadcrumb - internal val callRootSpan: ISpan? + internal val callSpan: ISpan? private var response: Response? = null private var clientErrorResponse: Response? = null - private val isReadingResponseBody = AtomicBoolean(false) private val isEventFinished = AtomicBoolean(false) private val url: String private val method: String @@ -50,10 +40,10 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req method = request.method // We start the call span that will contain all the others - val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span - callRootSpan = parentSpan?.startChild("http.client", "$method $url") - callRootSpan?.spanContext?.origin = TRACE_ORIGIN - urlDetails.applyToSpan(callRootSpan) + val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span + callSpan = parentSpan?.startChild("http.client", "$method $url") + callSpan?.spanContext?.origin = TRACE_ORIGIN + urlDetails.applyToSpan(callSpan) // We setup a breadcrumb with all meaningful data breadcrumb = Breadcrumb.http(url, method) @@ -62,43 +52,43 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req // needs this as unix timestamp for rrweb breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis) - // We add the same data to the root call span - callRootSpan?.setData("url", url) - callRootSpan?.setData("host", host) - callRootSpan?.setData("path", encodedPath) - callRootSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) + // We add the same data to the call span + callSpan?.setData("url", url) + callSpan?.setData("host", host) + callSpan?.setData("path", encodedPath) + callSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.uppercase(Locale.ROOT)) } /** * Sets the [Response] that will be sent in the breadcrumb [Hint]. - * Also, it sets the protocol and status code in the breadcrumb and the call root span. + * Also, it sets the protocol and status code in the breadcrumb and the call span. */ fun setResponse(response: Response) { this.response = response breadcrumb.setData(PROTOCOL_KEY, response.protocol.name) breadcrumb.setData("status_code", response.code) - callRootSpan?.setData(PROTOCOL_KEY, response.protocol.name) - callRootSpan?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) + callSpan?.setData(PROTOCOL_KEY, response.protocol.name) + callSpan?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) } fun setProtocol(protocolName: String?) { if (protocolName != null) { breadcrumb.setData(PROTOCOL_KEY, protocolName) - callRootSpan?.setData(PROTOCOL_KEY, protocolName) + callSpan?.setData(PROTOCOL_KEY, protocolName) } } fun setRequestBodySize(byteCount: Long) { if (byteCount > -1) { breadcrumb.setData("request_content_length", byteCount) - callRootSpan?.setData("http.request_content_length", byteCount) + callSpan?.setData("http.request_content_length", byteCount) } } fun setResponseBodySize(byteCount: Long) { if (byteCount > -1) { breadcrumb.setData("response_content_length", byteCount) - callRootSpan?.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) + callSpan?.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) } } @@ -110,44 +100,33 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req fun setError(errorMessage: String?) { if (errorMessage != null) { breadcrumb.setData(ERROR_MESSAGE_KEY, errorMessage) - callRootSpan?.setData(ERROR_MESSAGE_KEY, errorMessage) + callSpan?.setData(ERROR_MESSAGE_KEY, errorMessage) } } - /** Starts a span, if the callRootSpan is not null. */ - fun startSpan(event: String) { - // Find the parent of the span being created. E.g. secureConnect is child of connect - val parentSpan = findParentSpan(event) - val span = parentSpan?.startChild("http.client.$event", "$method $url") ?: return - if (event == RESPONSE_BODY_EVENT) { - // We save this event is reading the response body, so that it will not be auto-finished - isReadingResponseBody.set(true) - } - span.spanContext.origin = TRACE_ORIGIN - eventSpans[event] = span + /** Record event start if the callRootSpan is not null. */ + fun onEventStart(event: String) { + callSpan ?: return + eventDates[event] = scopes.options.dateProvider.now() } - /** Finishes a previously started span, and runs [beforeFinish] on it, on its parent and on the call root span. */ - fun finishSpan(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null): ISpan? { - val span = eventSpans[event] ?: return null - val parentSpan = findParentSpan(event) - beforeFinish?.invoke(span) - moveThrowableToRootSpan(span) - if (parentSpan != null && parentSpan != callRootSpan) { - beforeFinish?.invoke(parentSpan) - moveThrowableToRootSpan(parentSpan) - } - callRootSpan?.let { beforeFinish?.invoke(it) } - span.finish() - return span + /** Record event finish and runs [beforeFinish] on the call span. */ + fun onEventFinish(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null) { + val eventDate = eventDates.remove(event) ?: return + callSpan ?: return + beforeFinish?.invoke(callSpan) + val eventDurationNanos = scopes.options.dateProvider.now().diff(eventDate) + callSpan.setData(event, TimeUnit.NANOSECONDS.toMillis(eventDurationNanos)) } - /** Finishes the call root span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */ - fun finishEvent(finishDate: SentryDate? = null, beforeFinish: ((span: ISpan) -> Unit)? = null) { + /** Finishes the call span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */ + fun finish(beforeFinish: ((span: ISpan) -> Unit)? = null) { // If the event already finished, we don't do anything if (isEventFinished.getAndSet(true)) { return } + // We clear any date left, in case an event started, but never finished. Shouldn't happen. + eventDates.clear() // We put data in the hint and send a breadcrumb val hint = Hint() hint.set(TypeCheckHint.OKHTTP_REQUEST, request) @@ -156,77 +135,14 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req // needs this as unix timestamp for rrweb breadcrumb.setData(SpanDataConvention.HTTP_END_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis) // We send the breadcrumb even without spans. - hub.addBreadcrumb(breadcrumb, hint) - - // No span is created (e.g. no transaction is running) - if (callRootSpan == null) { - // We report the client error even without spans. - clientErrorResponse?.let { - SentryOkHttpUtils.captureClientError(hub, it.request, it) - } - return - } + scopes.addBreadcrumb(breadcrumb, hint) - // We forcefully finish all spans, even if they should already have been finished through finishSpan() - eventSpans.values.filter { !it.isFinished }.forEach { - moveThrowableToRootSpan(it) - if (finishDate != null) { - it.finish(it.status, finishDate) - } else { - it.finish() - } - } - beforeFinish?.invoke(callRootSpan) - // We report the client error here, after all sub-spans finished, so that it will be bound - // to the root call span. + callSpan?.let { beforeFinish?.invoke(it) } + // We report the client error here so that it will be bound to the call span. We send it even if there is no running span. clientErrorResponse?.let { - SentryOkHttpUtils.captureClientError(hub, it.request, it) - } - if (finishDate != null) { - callRootSpan.finish(callRootSpan.status, finishDate) - } else { - callRootSpan.finish() + SentryOkHttpUtils.captureClientError(scopes, it.request, it) } + callSpan?.finish() return } - - /** Move any throwable from an inner span to the call root span. */ - private fun moveThrowableToRootSpan(span: ISpan) { - if (span != callRootSpan && span.throwable != null && span.status != null) { - callRootSpan?.throwable = span.throwable - callRootSpan?.status = span.status - span.throwable = null - } - } - - private fun findParentSpan(event: String): ISpan? = when (event) { - // PROXY_SELECT, DNS, CONNECT and CONNECTION are not children of one another - SECURE_CONNECT_EVENT -> eventSpans[CONNECT_EVENT] - REQUEST_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT] - REQUEST_BODY_EVENT -> eventSpans[CONNECTION_EVENT] - RESPONSE_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT] - RESPONSE_BODY_EVENT -> eventSpans[CONNECTION_EVENT] - else -> callRootSpan - } ?: callRootSpan - - fun scheduleFinish(timestamp: SentryDate) { - try { - hub.options.executorService.schedule({ - if (!isReadingResponseBody.get() && - (eventSpans.values.all { it.isFinished } || callRootSpan?.isFinished != true) - ) { - finishEvent(timestamp) - } - }, RESPONSE_BODY_TIMEOUT_MILLIS) - } catch (e: RejectedExecutionException) { - hub.options - .logger - .log( - SentryLevel.ERROR, - "Failed to call the executor. OkHttp span will not be finished " + - "automatically. Did you call Sentry.close()?", - e - ) - } - } } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt index 853d9cd02af..9e6ed3019f9 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt @@ -1,7 +1,7 @@ package io.sentry.okhttp -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes +import io.sentry.ScopesAdapter import io.sentry.SpanDataConvention import io.sentry.SpanStatus import okhttp3.Call @@ -41,48 +41,48 @@ import java.util.concurrent.ConcurrentHashMap */ @Suppress("TooManyFunctions") public open class SentryOkHttpEventListener( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null ) : EventListener() { private var originalEventListener: EventListener? = null public companion object { - internal const val PROXY_SELECT_EVENT = "proxy_select" - internal const val DNS_EVENT = "dns" - internal const val SECURE_CONNECT_EVENT = "secure_connect" - internal const val CONNECT_EVENT = "connect" - internal const val CONNECTION_EVENT = "connection" - internal const val REQUEST_HEADERS_EVENT = "request_headers" - internal const val REQUEST_BODY_EVENT = "request_body" - internal const val RESPONSE_HEADERS_EVENT = "response_headers" - internal const val RESPONSE_BODY_EVENT = "response_body" + internal const val PROXY_SELECT_EVENT = "http.client.proxy_select_ms" + internal const val DNS_EVENT = "http.client.resolve_dns_ms" + internal const val CONNECT_EVENT = "http.connect_ms" + internal const val SECURE_CONNECT_EVENT = "http.connect.secure_connect_ms" + internal const val CONNECTION_EVENT = "http.connection_ms" + internal const val REQUEST_HEADERS_EVENT = "http.connection.request_headers_ms" + internal const val REQUEST_BODY_EVENT = "http.connection.request_body_ms" + internal const val RESPONSE_HEADERS_EVENT = "http.connection.response_headers_ms" + internal const val RESPONSE_BODY_EVENT = "http.connection.response_body_ms" internal val eventMap: MutableMap = ConcurrentHashMap() } public constructor() : this( - HubAdapter.getInstance(), + ScopesAdapter.getInstance(), originalEventListenerCreator = null ) public constructor(originalEventListener: EventListener) : this( - HubAdapter.getInstance(), + ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListener } ) public constructor(originalEventListenerFactory: Factory) : this( - HubAdapter.getInstance(), + ScopesAdapter.getInstance(), originalEventListenerCreator = { originalEventListenerFactory.create(it) } ) - public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListener: EventListener) : this( - hub, + public constructor(scopes: IScopes = ScopesAdapter.getInstance(), originalEventListener: EventListener) : this( + scopes, originalEventListenerCreator = { originalEventListener } ) - public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListenerFactory: Factory) : this( - hub, + public constructor(scopes: IScopes = ScopesAdapter.getInstance(), originalEventListenerFactory: Factory) : this( + scopes, originalEventListenerCreator = { originalEventListenerFactory.create(it) } ) @@ -92,7 +92,7 @@ public open class SentryOkHttpEventListener( // If the wrapped EventListener is ours, we can just delegate the calls, // without creating other events that would create duplicates if (canCreateEventSpan()) { - eventMap[call] = SentryOkHttpEvent(hub, call.request()) + eventMap[call] = SentryOkHttpEvent(scopes, call.request()) } } @@ -102,7 +102,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(PROXY_SELECT_EVENT) + okHttpEvent.onEventStart(PROXY_SELECT_EVENT) } override fun proxySelectEnd( @@ -115,7 +115,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(PROXY_SELECT_EVENT) { + okHttpEvent.onEventFinish(PROXY_SELECT_EVENT) { if (proxies.isNotEmpty()) { it.setData("proxies", proxies.joinToString { proxy -> proxy.toString() }) } @@ -128,7 +128,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(DNS_EVENT) + okHttpEvent.onEventStart(DNS_EVENT) } override fun dnsEnd( @@ -141,7 +141,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(DNS_EVENT) { + okHttpEvent.onEventFinish(DNS_EVENT) { it.setData("domain_name", domainName) if (inetAddressList.isNotEmpty()) { it.setData("dns_addresses", inetAddressList.joinToString { address -> address.toString() }) @@ -159,7 +159,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECT_EVENT) + okHttpEvent.onEventStart(CONNECT_EVENT) } override fun secureConnectStart(call: Call) { @@ -168,7 +168,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(SECURE_CONNECT_EVENT) + okHttpEvent.onEventStart(SECURE_CONNECT_EVENT) } override fun secureConnectEnd(call: Call, handshake: Handshake?) { @@ -177,7 +177,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(SECURE_CONNECT_EVENT) + okHttpEvent.onEventFinish(SECURE_CONNECT_EVENT) } override fun connectEnd( @@ -192,7 +192,7 @@ public open class SentryOkHttpEventListener( } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return okHttpEvent.setProtocol(protocol?.name) - okHttpEvent.finishSpan(CONNECT_EVENT) + okHttpEvent.onEventFinish(CONNECT_EVENT) } override fun connectFailed( @@ -209,7 +209,7 @@ public open class SentryOkHttpEventListener( val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return okHttpEvent.setProtocol(protocol?.name) okHttpEvent.setError(ioe.message) - okHttpEvent.finishSpan(CONNECT_EVENT) { + okHttpEvent.onEventFinish(CONNECT_EVENT) { it.throwable = ioe it.status = SpanStatus.INTERNAL_ERROR } @@ -221,7 +221,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECTION_EVENT) + okHttpEvent.onEventStart(CONNECTION_EVENT) } override fun connectionReleased(call: Call, connection: Connection) { @@ -230,7 +230,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(CONNECTION_EVENT) + okHttpEvent.onEventFinish(CONNECTION_EVENT) } override fun requestHeadersStart(call: Call) { @@ -239,7 +239,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_HEADERS_EVENT) + okHttpEvent.onEventStart(REQUEST_HEADERS_EVENT) } override fun requestHeadersEnd(call: Call, request: Request) { @@ -248,7 +248,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) + okHttpEvent.onEventFinish(REQUEST_HEADERS_EVENT) } override fun requestBodyStart(call: Call) { @@ -257,7 +257,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_BODY_EVENT) + okHttpEvent.onEventStart(REQUEST_BODY_EVENT) } override fun requestBodyEnd(call: Call, byteCount: Long) { @@ -266,7 +266,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + okHttpEvent.onEventFinish(REQUEST_BODY_EVENT) { if (byteCount > 0) { it.setData("http.request_content_length", byteCount) } @@ -283,13 +283,13 @@ public open class SentryOkHttpEventListener( okHttpEvent.setError(ioe.message) // requestFailed can happen after requestHeaders or requestBody. // If requestHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) { + okHttpEvent.onEventFinish(REQUEST_HEADERS_EVENT) { if (!it.isFinished) { it.status = SpanStatus.INTERNAL_ERROR it.throwable = ioe } } - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + okHttpEvent.onEventFinish(REQUEST_BODY_EVENT) { it.status = SpanStatus.INTERNAL_ERROR it.throwable = ioe } @@ -301,7 +301,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_HEADERS_EVENT) + okHttpEvent.onEventStart(RESPONSE_HEADERS_EVENT) } override fun responseHeadersEnd(call: Call, response: Response) { @@ -311,14 +311,13 @@ public open class SentryOkHttpEventListener( } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return okHttpEvent.setResponse(response) - val responseHeadersSpan = okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + okHttpEvent.onEventFinish(RESPONSE_HEADERS_EVENT) { it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) // Let's not override the status of a span that was set if (it.status == null) { it.status = SpanStatus.fromHttpStatusCode(response.code) } } - okHttpEvent.scheduleFinish(responseHeadersSpan?.finishDate ?: hub.options.dateProvider.now()) } override fun responseBodyStart(call: Call) { @@ -327,7 +326,7 @@ public open class SentryOkHttpEventListener( return } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_BODY_EVENT) + okHttpEvent.onEventStart(RESPONSE_BODY_EVENT) } override fun responseBodyEnd(call: Call, byteCount: Long) { @@ -337,7 +336,7 @@ public open class SentryOkHttpEventListener( } val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return okHttpEvent.setResponseBodySize(byteCount) - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + okHttpEvent.onEventFinish(RESPONSE_BODY_EVENT) { if (byteCount > 0) { it.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) } @@ -353,13 +352,13 @@ public open class SentryOkHttpEventListener( okHttpEvent.setError(ioe.message) // responseFailed can happen after responseHeaders or responseBody. // If responseHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + okHttpEvent.onEventFinish(RESPONSE_HEADERS_EVENT) { if (!it.isFinished) { it.status = SpanStatus.INTERNAL_ERROR it.throwable = ioe } } - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + okHttpEvent.onEventFinish(RESPONSE_BODY_EVENT) { it.status = SpanStatus.INTERNAL_ERROR it.throwable = ioe } @@ -368,7 +367,7 @@ public open class SentryOkHttpEventListener( override fun callEnd(call: Call) { originalEventListener?.callEnd(call) val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return - okHttpEvent.finishEvent() + okHttpEvent.finish() } override fun callFailed(call: Call, ioe: IOException) { @@ -378,7 +377,7 @@ public open class SentryOkHttpEventListener( } val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return okHttpEvent.setError(ioe.message) - okHttpEvent.finishEvent { + okHttpEvent.finish { it.status = SpanStatus.INTERNAL_ERROR it.throwable = ioe } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index 601e6d56d48..370b3ccb6bc 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -4,9 +4,9 @@ import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.HttpStatusCodeRange -import io.sentry.HubAdapter -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan +import io.sentry.ScopesAdapter import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SpanDataConvention @@ -18,6 +18,7 @@ import io.sentry.transport.CurrentDateProvider import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.Platform import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.SpanUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import okhttp3.Interceptor @@ -30,7 +31,7 @@ import java.io.IOException * out of the active span bound to the scope for each HTTP Request. * If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well. * - * @param hub The [IHub], internal and only used for testing. + * @param scopes The [IScopes], internal and only used for testing. * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, * Defaults to true. @@ -40,7 +41,7 @@ import java.io.IOException * is a match for any of the defined targets. */ public open class SentryOkHttpInterceptor( - private val hub: IHub = HubAdapter.getInstance(), + private val scopes: IScopes = ScopesAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, private val captureFailedRequests: Boolean = true, private val failedRequestStatusCodes: List = listOf( @@ -49,9 +50,9 @@ public open class SentryOkHttpInterceptor( private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) ) : Interceptor { - public constructor() : this(HubAdapter.getInstance()) - public constructor(hub: IHub) : this(hub, null) - public constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) + public constructor() : this(ScopesAdapter.getInstance()) + public constructor(scopes: IScopes) : this(scopes, null) + public constructor(beforeSpan: BeforeSpanCallback) : this(ScopesAdapter.getInstance(), beforeSpan) init { addIntegrationToSdkVersion("OkHttp") @@ -73,11 +74,11 @@ public open class SentryOkHttpInterceptor( if (SentryOkHttpEventListener.eventMap.containsKey(chain.call())) { // read the span from the event listener okHttpEvent = SentryOkHttpEventListener.eventMap[chain.call()] - span = okHttpEvent?.callRootSpan + span = okHttpEvent?.callSpan } else { // read the span from the bound scope okHttpEvent = null - val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span span = parentSpan?.startChild("http.client", "$method $url") } val startTimestamp = CurrentDateProvider.getInstance().currentTimeMillis @@ -93,16 +94,21 @@ public open class SentryOkHttpInterceptor( try { val requestBuilder = request.newBuilder() - TracingUtils.traceIfAllowed( - hub, - request.url.toString(), - request.headers(BaggageHeader.BAGGAGE_HEADER), - span - )?.let { tracingHeaders -> - requestBuilder.addHeader(tracingHeaders.sentryTraceHeader.name, tracingHeaders.sentryTraceHeader.value) - tracingHeaders.baggageHeader?.let { - requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) - requestBuilder.addHeader(it.name, it.value) + if (!isIgnored()) { + TracingUtils.traceIfAllowed( + scopes, + request.url.toString(), + request.headers(BaggageHeader.BAGGAGE_HEADER), + span + )?.let { tracingHeaders -> + requestBuilder.addHeader( + tracingHeaders.sentryTraceHeader.name, + tracingHeaders.sentryTraceHeader.value + ) + tracingHeaders.baggageHeader?.let { + requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) + requestBuilder.addHeader(it.name, it.value) + } } } @@ -123,7 +129,7 @@ public open class SentryOkHttpInterceptor( if (isFromEventListener && okHttpEvent != null) { okHttpEvent.setClientErrorResponse(response) } else { - SentryOkHttpUtils.captureClientError(hub, request, response) + SentryOkHttpUtils.captureClientError(scopes, request, response) } } @@ -135,7 +141,7 @@ public open class SentryOkHttpInterceptor( } throw e } finally { - finishSpan(span, request, response, isFromEventListener) + finishSpan(span, request, response, isFromEventListener, okHttpEvent) // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call if (!isFromEventListener) { @@ -144,6 +150,10 @@ public open class SentryOkHttpInterceptor( } } + private fun isIgnored(): Boolean { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN) + } + private fun sendBreadcrumb( request: Request, code: Int?, @@ -167,10 +177,10 @@ public open class SentryOkHttpInterceptor( breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, startTimestamp) breadcrumb.setData(SpanDataConvention.HTTP_END_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis) - hub.addBreadcrumb(breadcrumb, hint) + scopes.addBreadcrumb(breadcrumb, hint) } - private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean) { + private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean, okHttpEvent: SentryOkHttpEvent?) { if (span == null) { return } @@ -180,16 +190,12 @@ public open class SentryOkHttpInterceptor( // span is dropped span.spanContext.sampled = false } - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } - } else { - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } } + if (!isFromEventListener) { + span.finish() + } + // The SentryOkHttpEventListener waits until the response is closed (which may never happen), so we close it here + okHttpEvent?.finish() } private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt index 0cfc1c5a755..eea35ca22e6 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt @@ -1,7 +1,7 @@ package io.sentry.okhttp import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.TypeCheckHint import io.sentry.exception.ExceptionMechanismException @@ -15,7 +15,7 @@ import okhttp3.Response internal object SentryOkHttpUtils { - internal fun captureClientError(hub: IHub, request: Request, response: Response) { + internal fun captureClientError(scopes: IScopes, request: Request, response: Response) { // not possible to get a parameterized url, but we remove at least the // query string and the fragment. // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query @@ -40,9 +40,9 @@ internal object SentryOkHttpUtils { val sentryRequest = io.sentry.protocol.Request().apply { urlDetails.applyToRequest(this) // Cookie is only sent if isSendDefaultPii is enabled - cookies = if (hub.options.isSendDefaultPii) request.headers["Cookie"] else null + cookies = if (scopes.options.isSendDefaultPii) request.headers["Cookie"] else null method = request.method - headers = getHeaders(hub, request.headers) + headers = getHeaders(scopes, request.headers) request.body?.contentLength().ifHasValidLength { bodySize = it @@ -51,8 +51,8 @@ internal object SentryOkHttpUtils { val sentryResponse = io.sentry.protocol.Response().apply { // Set-Cookie is only sent if isSendDefaultPii is enabled due to PII - cookies = if (hub.options.isSendDefaultPii) response.headers["Set-Cookie"] else null - headers = getHeaders(hub, response.headers) + cookies = if (scopes.options.isSendDefaultPii) response.headers["Set-Cookie"] else null + headers = getHeaders(scopes, response.headers) statusCode = response.code response.body?.contentLength().ifHasValidLength { @@ -63,7 +63,7 @@ internal object SentryOkHttpUtils { event.request = sentryRequest event.contexts.setResponse(sentryResponse) - hub.captureEvent(event, hint) + scopes.captureEvent(event, hint) } private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { @@ -72,9 +72,9 @@ internal object SentryOkHttpUtils { } } - private fun getHeaders(hub: IHub, requestHeaders: Headers): MutableMap? { + private fun getHeaders(scopes: IScopes, requestHeaders: Headers): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII - if (!hub.options.isSendDefaultPii) { + if (!scopes.options.isSendDefaultPii) { return null } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt index 88727c4c0d7..b1e5fb52262 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt @@ -1,14 +1,14 @@ package io.sentry.okhttp import io.sentry.BaggageHeader -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext -import io.sentry.test.ImmediateExecutorService +import io.sentry.mockServerRequestTimeoutMillis import okhttp3.Call import okhttp3.EventListener import okhttp3.OkHttpClient @@ -23,10 +23,10 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -36,7 +36,7 @@ import kotlin.test.assertTrue class SentryOkHttpEventListenerTest { class Fixture { - val hub = mock() + val scopes = mock() val server = MockWebServer() val mockEventListener = mock() val mockEventListenerFactory = mock() @@ -63,12 +63,12 @@ class SentryOkHttpEventListenerTest { isSendDefaultPii = sendDefaultPii configureOptions(this) } - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } server.enqueue( MockResponse() @@ -80,12 +80,12 @@ class SentryOkHttpEventListenerTest { val builder = OkHttpClient.Builder() if (useInterceptor) { - builder.addInterceptor(SentryOkHttpInterceptor(hub)) + builder.addInterceptor(SentryOkHttpInterceptor(scopes)) } sentryOkHttpEventListener = when { - eventListenerFactory != null -> SentryOkHttpEventListener(hub, eventListenerFactory) - eventListener != null -> SentryOkHttpEventListener(hub, eventListener) - else -> SentryOkHttpEventListener(hub) + eventListenerFactory != null -> SentryOkHttpEventListener(scopes, eventListenerFactory) + eventListener != null -> SentryOkHttpEventListener(scopes, eventListener) + else -> SentryOkHttpEventListener(scopes) } return builder.eventListener(sentryOkHttpEventListener).build() } @@ -113,7 +113,7 @@ class SentryOkHttpEventListenerTest { fun `when there is an active span and the SentryOkHttpInterceptor, adds sentry trace headers to the request`() { val sut = fixture.getSut(useInterceptor = true) sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -123,7 +123,7 @@ class SentryOkHttpEventListenerTest { fun `when there is an active span but no SentryOkHttpInterceptor, sentry trace headers are not added to the request`() { val sut = fixture.getSut() sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -135,7 +135,7 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan response.close() assertNotNull(callSpan) assertEquals(callSpan, fixture.sentryTracer.children.first()) @@ -146,76 +146,40 @@ class SentryOkHttpEventListenerTest { } @Test - fun `creates a span for each event`() { + fun `adds a data for each event`() { val sut = fixture.getSut() val request = getRequest() val call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan response.close() - assertEquals(8, fixture.sentryTracer.children.size) - fixture.sentryTracer.children.forEachIndexed { index, span -> - assertTrue(span.isFinished) - when (index) { - 0 -> { - assertEquals(callSpan, span) - assertEquals("GET ${request.url}", span.description) - assertNotNull(span.data["proxies"]) - assertNotNull(span.data["domain_name"]) - assertNotNull(span.data["dns_addresses"]) - assertEquals(201, span.data[SpanDataConvention.HTTP_STATUS_CODE_KEY]) - } - 1 -> { - assertEquals("http.client.proxy_select", span.operation) - assertEquals("GET ${request.url}", span.description) - assertNotNull(span.data["proxies"]) - } - 2 -> { - assertEquals("http.client.dns", span.operation) - assertEquals("GET ${request.url}", span.description) - assertNotNull(span.data["domain_name"]) - assertNotNull(span.data["dns_addresses"]) - } - 3 -> { - assertEquals("http.client.connect", span.operation) - assertEquals("GET ${request.url}", span.description) - } - 4 -> { - assertEquals("http.client.connection", span.operation) - assertEquals("GET ${request.url}", span.description) - } - 5 -> { - assertEquals("http.client.request_headers", span.operation) - assertEquals("GET ${request.url}", span.description) - } - 6 -> { - assertEquals("http.client.response_headers", span.operation) - assertEquals("GET ${request.url}", span.description) - assertEquals(201, span.data[SpanDataConvention.HTTP_STATUS_CODE_KEY]) - } - 7 -> { - assertEquals("http.client.response_body", span.operation) - assertEquals("GET ${request.url}", span.description) - } - } - } + assertEquals(1, fixture.sentryTracer.children.size) + assertNotNull(callSpan) + assertNotNull(callSpan.getData("proxies")) + assertNotNull(callSpan.getData("domain_name")) + assertNotNull(callSpan.getData("dns_addresses")) + assertEquals(201, callSpan.getData(SpanDataConvention.HTTP_STATUS_CODE_KEY)) + assertNotNull(callSpan.getData("http.client.proxy_select_ms")) + assertNotNull(callSpan.getData("http.client.resolve_dns_ms")) + assertNotNull(callSpan.getData("http.connect_ms")) + assertNotNull(callSpan.getData("http.connection_ms")) + assertNotNull(callSpan.getData("http.connection.request_headers_ms")) + assertNotNull(callSpan.getData("http.connection.response_headers_ms")) + assertNotNull(callSpan.getData("http.connection.response_body_ms")) } @Test - fun `has requestBody span for requests with body`() { + fun `has requestBody data for requests with body`() { val sut = fixture.getSut() val requestBody = "request body sent in the request" val request = postRequest(body = requestBody) val call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan response.close() - assertEquals(9, fixture.sentryTracer.children.size) - val requestBodySpan = fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.request_body" } - assertNotNull(requestBodySpan) - assertEquals(requestBody.toByteArray().size.toLong(), requestBodySpan.data["http.request_content_length"]) + assertNotNull(callSpan?.getData("http.connection.request_body_ms")) assertEquals(requestBody.toByteArray().size.toLong(), callSpan?.getData("http.request_content_length")) } @@ -227,19 +191,15 @@ class SentryOkHttpEventListenerTest { val call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan + assertNull(callSpan?.getData("http.connection.response_body_ms")) // Consume the response val responseBytes = response.body?.byteStream()?.readBytes() assertNotNull(responseBytes) + assertNotNull(callSpan?.getData("http.connection.response_body_ms")) response.close() - val requestBodySpan = fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.response_body" } - assertNotNull(requestBodySpan) - assertEquals( - responseBytes.size.toLong(), - requestBodySpan.data[SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY] - ) assertEquals( responseBytes.size.toLong(), callSpan?.getData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY) @@ -247,13 +207,13 @@ class SentryOkHttpEventListenerTest { } @Test - fun `root call span status depends on http status code`() { + fun `call span status depends on http status code`() { val sut = fixture.getSut(httpStatusCode = 404) val request = getRequest() val call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan response.close() assertNotNull(callSpan) assertEquals(SpanStatus.fromHttpStatusCode(404), callSpan.status) @@ -283,7 +243,7 @@ class SentryOkHttpEventListenerTest { @Test fun `propagate all calls to the SentryOkHttpEventListener passed in the ctor`() { - val originalListener = spy(SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener)) + val originalListener = spy(SentryOkHttpEventListener(fixture.scopes, fixture.mockEventListener)) val sut = fixture.getSut(eventListener = originalListener) val listener = fixture.sentryOkHttpEventListener val request = postRequest(body = "requestBody") @@ -295,7 +255,7 @@ class SentryOkHttpEventListenerTest { @Test fun `propagate all calls to the SentryOkHttpEventListener factory passed in the ctor`() { - val originalListener = spy(SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener)) + val originalListener = spy(SentryOkHttpEventListener(fixture.scopes, fixture.mockEventListener)) val sut = fixture.getSut(eventListenerFactory = { originalListener }) val listener = fixture.sentryOkHttpEventListener val request = postRequest(body = "requestBody") @@ -307,86 +267,23 @@ class SentryOkHttpEventListenerTest { @Test fun `does not duplicated spans if an SentryOkHttpEventListener is passed in the ctor`() { - val originalListener = spy(SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener)) + val originalListener = spy(SentryOkHttpEventListener(fixture.scopes, fixture.mockEventListener)) val sut = fixture.getSut(eventListener = originalListener) val request = postRequest(body = "requestBody") val call = sut.newCall(request) val response = call.execute() response.close() // Spans are created by the originalListener, so the listener doesn't create duplicates - assertEquals(9, fixture.sentryTracer.children.size) - } - - @Test - fun `status propagates to parent span and call root span`() { - val sut = fixture.getSut(httpStatusCode = 500) - val request = getRequest() - val call = sut.newCall(request) - val response = call.execute() - val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan - val responseHeaderSpan = - fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.response_headers" } - val connectionSpan = fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.connection" } - response.close() - assertNotNull(callSpan) - assertNotNull(responseHeaderSpan) - assertNotNull(connectionSpan) - assertEquals(SpanStatus.fromHttpStatusCode(500), callSpan.status) - assertEquals(SpanStatus.fromHttpStatusCode(500), responseHeaderSpan.status) - assertEquals(SpanStatus.fromHttpStatusCode(500), connectionSpan.status) - } - - @Test - fun `when response is not closed, root call is trimmed to responseHeadersEnd`() { - val sut = fixture.getSut( - httpStatusCode = 500, - configureOptions = { it.executorService = ImmediateExecutorService() } - ) - val request = getRequest() - val call = sut.newCall(request) - val response = spy(call.execute()) - val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan - val responseHeaderSpan = - fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.response_headers" } - val responseBodySpan = fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.response_body" } - - // Response is not finished - verify(response, never()).close() - - // response body span is never started - assertNull(responseBodySpan) - - assertNotNull(callSpan) - assertNotNull(responseHeaderSpan) - - // Call span is trimmed to responseHeader finishTimestamp - assertEquals(callSpan.finishDate?.nanoTimestamp(), responseHeaderSpan.finishDate?.nanoTimestamp()) - - // All children spans of the root call are finished - assertTrue(fixture.sentryTracer.children.all { it.isFinished }) - } - - @Test - fun `responseHeadersEnd schedules event finish`() { - val listener = SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener) - whenever(fixture.hub.options).thenReturn(SentryOptions()) - val call = mock() - whenever(call.request()).thenReturn(getRequest()) - val okHttpEvent = mock() - SentryOkHttpEventListener.eventMap[call] = okHttpEvent - listener.responseHeadersEnd(call, mock()) - verify(okHttpEvent).scheduleFinish(any()) + assertEquals(1, fixture.sentryTracer.children.size) } @Test - fun `call root span status is not overridden if not null`() { + fun `call span status is not overridden if not null`() { val mockListener = mock() lateinit var call: Call whenever(mockListener.connectStart(any(), anyOrNull(), anyOrNull())).then { val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan assertNotNull(callSpan) assertNull(callSpan.status) callSpan.status = SpanStatus.UNKNOWN @@ -397,7 +294,7 @@ class SentryOkHttpEventListenerTest { call = sut.newCall(request) val response = call.execute() val okHttpEvent = SentryOkHttpEventListener.eventMap[call] - val callSpan = okHttpEvent?.callRootSpan + val callSpan = okHttpEvent?.callSpan assertNotNull(callSpan) response.close() assertEquals(SpanStatus.UNKNOWN, callSpan.status) diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index 4b41b75c1e3..33f9b04d85f 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -2,8 +2,7 @@ package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint -import io.sentry.IHub -import io.sentry.ISentryExecutorService +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryDate import io.sentry.SentryOptions @@ -11,19 +10,10 @@ import io.sentry.SentryTracer import io.sentry.Span import io.sentry.SpanDataConvention import io.sentry.SpanOptions -import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.TypeCheckHint import io.sentry.exception.SentryHttpClientException -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT -import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT -import io.sentry.test.ImmediateExecutorService import io.sentry.test.getProperty import okhttp3.Protocol import okhttp3.Request @@ -33,14 +23,11 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.check -import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never -import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.concurrent.RejectedExecutionException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -50,14 +37,14 @@ import kotlin.test.assertTrue class SentryOkHttpEventTest { private class Fixture { - val hub = mock() + val scopes = mock() val server = MockWebServer() val span: ISpan val mockRequest: Request val response: Response init { - whenever(hub.options).thenReturn( + whenever(scopes.options).thenReturn( SentryOptions().apply { dsn = "https://key@sentry.io/proj" } @@ -65,9 +52,8 @@ class SentryOkHttpEventTest { span = Span( TransactionContext("name", "op", TracesSamplingDecision(true)), - SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), hub), - hub, - null, + SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), scopes), + scopes, SpanOptions() ) @@ -86,7 +72,7 @@ class SentryOkHttpEventTest { } fun getSut(currentSpan: ISpan? = span, requestUrl: String ? = null): SentryOkHttpEvent { - whenever(hub.span).thenReturn(currentSpan) + whenever(scopes.span).thenReturn(currentSpan) val request = if (requestUrl == null) { mockRequest } else { @@ -96,22 +82,22 @@ class SentryOkHttpEventTest { .url(server.url(requestUrl)) .build() } - return SentryOkHttpEvent(hub, request) + return SentryOkHttpEvent(scopes, request) } } private val fixture = Fixture() @Test - fun `when there is no active span, root span is null`() { + fun `when there is no active span, call span is null`() { val sut = fixture.getSut(currentSpan = null) - assertNull(sut.callRootSpan) + assertNull(sut.callSpan) } @Test - fun `when there is an active span, a root span is created`() { + fun `when there is an active span, a call span is created`() { val sut = fixture.getSut() - val callSpan = sut.callRootSpan + val callSpan = sut.callSpan assertNotNull(callSpan) assertEquals("http.client", callSpan.operation) assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", callSpan.description) @@ -122,127 +108,120 @@ class SentryOkHttpEventTest { } @Test - fun `when root span is null, breadcrumb is created anyway`() { + fun `when call span is null, breadcrumb is created anyway`() { val sut = fixture.getSut(currentSpan = null) - assertNull(sut.callRootSpan) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb(any(), anyOrNull()) + assertNull(sut.callSpan) + sut.finish() + verify(fixture.scopes).addBreadcrumb(any(), anyOrNull()) } @Test - fun `when root span is null, no span is created`() { + fun `when call span is null, no event is recorded`() { val sut = fixture.getSut(currentSpan = null) - assertNull(sut.callRootSpan) - sut.startSpan("span") - assertTrue(sut.getEventSpans().isEmpty()) + assertNull(sut.callSpan) + sut.onEventStart("span") + assertTrue(sut.getEventDates().isEmpty()) } @Test - fun `when event is finished, root span is finished`() { + fun `when event is finished, call span is finished`() { val sut = fixture.getSut() - val rootSpan = sut.callRootSpan + val rootSpan = sut.callSpan assertNotNull(rootSpan) assertFalse(rootSpan.isFinished) - sut.finishEvent() + sut.finish() assertTrue(rootSpan.isFinished) } @Test - fun `when startSpan, a new span is started`() { + fun `when onEventStart, a new event is recorded`() { val sut = fixture.getSut() - assertTrue(sut.getEventSpans().isEmpty()) - sut.startSpan("span") - val spans = sut.getEventSpans() - assertEquals(1, spans.size) - val span = spans["span"] - assertNotNull(span) - assertTrue(spans.containsKey("span")) - assertEquals("http.client.span", span.operation) - assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", span.description) - assertFalse(span.isFinished) + val callSpan = sut.callSpan + assertTrue(sut.getEventDates().isEmpty()) + sut.onEventStart("span") + val dates = sut.getEventDates() + assertEquals(1, dates.size) + assertNull(callSpan!!.getData("span")) } @Test - fun `when finishSpan, a span is finished if previously started`() { + fun `when onEventFinish, an event is added to call span`() { val sut = fixture.getSut() - assertTrue(sut.getEventSpans().isEmpty()) - sut.startSpan("span") - val spans = sut.getEventSpans() - assertFalse(spans["span"]!!.isFinished) - sut.finishSpan("span") - assertTrue(spans["span"]!!.isFinished) + val callSpan = sut.callSpan + assertTrue(sut.getEventDates().isEmpty()) + sut.onEventStart("span") + val dates = sut.getEventDates() + assertEquals(1, dates.size) + assertNull(callSpan!!.getData("span")) + sut.onEventFinish("span") + assertEquals(0, dates.size) + assertNotNull(callSpan.getData("span")) } @Test - fun `when finishSpan, a callback is called before the span is finished`() { + fun `when onEventFinish, a callback is called before the event is set`() { val sut = fixture.getSut() + val callSpan = sut.callSpan var called = false - assertTrue(sut.getEventSpans().isEmpty()) - sut.startSpan("span") - val spans = sut.getEventSpans() - assertFalse(spans["span"]!!.isFinished) - sut.finishSpan("span") { + assertTrue(sut.getEventDates().isEmpty()) + sut.onEventStart("span") + assertNull(callSpan!!.getData("span")) + sut.onEventFinish("span") { called = true - assertFalse(it.isFinished) + assertNull(callSpan.getData("span")) } - assertTrue(spans["span"]!!.isFinished) + assertNotNull(callSpan.getData("span")) assertTrue(called) } @Test - fun `when finishSpan, a callback is called with the current span and the root call span is finished`() { + fun `when onEventFinish, a callback is called only once with the call span`() { val sut = fixture.getSut() var called = 0 - sut.startSpan("span") - sut.finishSpan("span") { - if (called == 0) { - assertEquals("http.client.span", it.operation) - assertEquals("${fixture.mockRequest.method} ${fixture.mockRequest.url}", it.description) - } else { - assertEquals(sut.callRootSpan, it) - } + sut.onEventStart("span") + sut.onEventFinish("span") { called++ - assertFalse(it.isFinished) } - assertEquals(2, called) + assertEquals(1, called) } @Test - fun `finishSpan is ignored if the span was not previously started`() { + fun `onEventFinish is ignored if the span was not previously started`() { val sut = fixture.getSut() var called = false - assertTrue(sut.getEventSpans().isEmpty()) - sut.finishSpan("span") { called = true } - assertTrue(sut.getEventSpans().isEmpty()) + assertTrue(sut.getEventDates().isEmpty()) + sut.onEventFinish("span") { called = true } + assertTrue(sut.getEventDates().isEmpty()) assertFalse(called) } @Test - fun `when finishEvent, a callback is called with the call root span before it is finished`() { + fun `when finish, a callback is called with the call span before it is finished`() { val sut = fixture.getSut() var called = false - sut.finishEvent { + sut.finish { called = true - assertEquals(sut.callRootSpan, it) + assertEquals(sut.callSpan, it) + assertFalse(it.isFinished) } assertTrue(called) } @Test - fun `when finishEvent, all running spans are finished`() { + fun `when finish, all event dates are cleared`() { val sut = fixture.getSut() - sut.startSpan("span") - val spans = sut.getEventSpans() - assertFalse(spans["span"]!!.isFinished) - sut.finishEvent() - assertTrue(spans["span"]!!.isFinished) + sut.onEventStart("span") + val dates = sut.getEventDates() + assertFalse(dates.isEmpty()) + sut.finish() + assertTrue(dates.isEmpty()) } @Test - fun `when finishEvent, a breadcrumb is captured with request in the hint`() { + fun `when finish, a breadcrumb is captured with request in the hint`() { val sut = fixture.getSut() - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertEquals(fixture.mockRequest.url.toString(), it.data["url"]) assertEquals(fixture.mockRequest.url.host, it.data["host"]) @@ -256,36 +235,23 @@ class SentryOkHttpEventTest { } @Test - fun `when finishEvent multiple times, only one breadcrumb is captured`() { - val sut = fixture.getSut() - sut.finishEvent() - sut.finishEvent() - verify(fixture.hub, times(1)).addBreadcrumb(any(), any()) - } - - @Test - fun `when finishEvent, does not override running spans status if set`() { + fun `when finish multiple times, only one breadcrumb is captured`() { val sut = fixture.getSut() - sut.startSpan("span") - val spans = sut.getEventSpans() - assertNull(spans["span"]!!.status) - spans["span"]!!.status = SpanStatus.OK - assertEquals(SpanStatus.OK, spans["span"]!!.status) - sut.finishEvent() - assertTrue(spans["span"]!!.isFinished) - assertEquals(SpanStatus.OK, spans["span"]!!.status) + sut.finish() + sut.finish() + verify(fixture.scopes, times(1)).addBreadcrumb(any(), any()) } @Test - fun `setResponse set protocol and code in the breadcrumb and root span, and response in the hint`() { + fun `setResponse set protocol and code in the breadcrumb and call span, and response in the hint`() { val sut = fixture.getSut() sut.setResponse(fixture.response) - assertEquals(fixture.response.protocol.name, sut.callRootSpan?.getData("protocol")) - assertEquals(fixture.response.code, sut.callRootSpan?.getData(SpanDataConvention.HTTP_STATUS_CODE_KEY)) - sut.finishEvent() + assertEquals(fixture.response.protocol.name, sut.callSpan?.getData("protocol")) + assertEquals(fixture.response.code, sut.callSpan?.getData(SpanDataConvention.HTTP_STATUS_CODE_KEY)) + sut.finish() - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals(fixture.response.protocol.name, it.data["protocol"]) assertEquals(fixture.response.code, it.data["status_code"]) @@ -297,12 +263,12 @@ class SentryOkHttpEventTest { } @Test - fun `setProtocol set protocol in the breadcrumb and in the root span`() { + fun `setProtocol set protocol in the breadcrumb and in the call span`() { val sut = fixture.getSut() sut.setProtocol("protocol") - assertEquals("protocol", sut.callRootSpan?.getData("protocol")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertEquals("protocol", sut.callSpan?.getData("protocol")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertEquals("protocol", it.data["protocol"]) }, @@ -314,9 +280,9 @@ class SentryOkHttpEventTest { fun `setProtocol is ignored if protocol is null`() { val sut = fixture.getSut() sut.setProtocol(null) - assertNull(sut.callRootSpan?.getData("protocol")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertNull(sut.callSpan?.getData("protocol")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertNull(it.data["protocol"]) }, @@ -325,12 +291,12 @@ class SentryOkHttpEventTest { } @Test - fun `setRequestBodySize set RequestBodySize in the breadcrumb and in the root span`() { + fun `setRequestBodySize set RequestBodySize in the breadcrumb and in the call span`() { val sut = fixture.getSut() sut.setRequestBodySize(10) - assertEquals(10L, sut.callRootSpan?.getData("http.request_content_length")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertEquals(10L, sut.callSpan?.getData("http.request_content_length")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertEquals(10L, it.data["request_content_length"]) }, @@ -342,9 +308,9 @@ class SentryOkHttpEventTest { fun `setRequestBodySize is ignored if RequestBodySize is negative`() { val sut = fixture.getSut() sut.setRequestBodySize(-1) - assertNull(sut.callRootSpan?.getData("http.request_content_length")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertNull(sut.callSpan?.getData("http.request_content_length")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertNull(it.data["request_content_length"]) }, @@ -353,12 +319,12 @@ class SentryOkHttpEventTest { } @Test - fun `setResponseBodySize set ResponseBodySize in the breadcrumb and in the root span`() { + fun `setResponseBodySize set ResponseBodySize in the breadcrumb and in the call span`() { val sut = fixture.getSut() sut.setResponseBodySize(10) - assertEquals(10L, sut.callRootSpan?.getData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY)) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertEquals(10L, sut.callSpan?.getData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY)) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertEquals(10L, it.data["response_content_length"]) }, @@ -370,9 +336,9 @@ class SentryOkHttpEventTest { fun `setResponseBodySize is ignored if ResponseBodySize is negative`() { val sut = fixture.getSut() sut.setResponseBodySize(-1) - assertNull(sut.callRootSpan?.getData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY)) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertNull(sut.callSpan?.getData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY)) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertNull(it.data["response_content_length"]) }, @@ -381,12 +347,12 @@ class SentryOkHttpEventTest { } @Test - fun `setError set success to false and errorMessage in the breadcrumb and in the root span`() { + fun `setError set success to false and errorMessage in the breadcrumb and in the call span`() { val sut = fixture.getSut() sut.setError("errorMessage") - assertEquals("errorMessage", sut.callRootSpan?.getData("error_message")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertEquals("errorMessage", sut.callSpan?.getData("error_message")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertEquals("errorMessage", it.data["error_message"]) }, @@ -395,13 +361,13 @@ class SentryOkHttpEventTest { } @Test - fun `setError sets success to false in the breadcrumb and in the root span even if errorMessage is null`() { + fun `setError sets success to false in the breadcrumb and in the call span even if errorMessage is null`() { val sut = fixture.getSut() sut.setError(null) - assertNotNull(sut.callRootSpan) - assertNull(sut.callRootSpan.getData("error_message")) - sut.finishEvent() - verify(fixture.hub).addBreadcrumb( + assertNotNull(sut.callSpan) + assertNull(sut.callSpan.getData("error_message")) + sut.finish() + verify(fixture.scopes).addBreadcrumb( check { assertNull(it.data["error_message"]) }, @@ -410,167 +376,15 @@ class SentryOkHttpEventTest { } @Test - fun `secureConnect span is child of connect span`() { - val sut = fixture.getSut() - sut.startSpan(CONNECT_EVENT) - sut.startSpan(SECURE_CONNECT_EVENT) - val spans = sut.getEventSpans() - val secureConnectSpan = spans[SECURE_CONNECT_EVENT] as Span? - val connectSpan = spans[CONNECT_EVENT] as Span? - assertNotNull(secureConnectSpan) - assertNotNull(connectSpan) - assertEquals(connectSpan.spanId, secureConnectSpan.parentSpanId) - } - - @Test - fun `secureConnect span is child of root span if connect span is not available`() { - val sut = fixture.getSut() - sut.startSpan(SECURE_CONNECT_EVENT) - val spans = sut.getEventSpans() - val rootSpan = sut.callRootSpan as Span? - val secureConnectSpan = spans[SECURE_CONNECT_EVENT] as Span? - assertNotNull(secureConnectSpan) - assertNotNull(rootSpan) - assertEquals(rootSpan.spanId, secureConnectSpan.parentSpanId) - } - - @Test - fun `request and response spans are children of connection span`() { - val sut = fixture.getSut() - sut.startSpan(CONNECTION_EVENT) - sut.startSpan(REQUEST_HEADERS_EVENT) - sut.startSpan(REQUEST_BODY_EVENT) - sut.startSpan(RESPONSE_HEADERS_EVENT) - sut.startSpan(RESPONSE_BODY_EVENT) - val spans = sut.getEventSpans() - val connectionSpan = spans[CONNECTION_EVENT] as Span? - val requestHeadersSpan = spans[REQUEST_HEADERS_EVENT] as Span? - val requestBodySpan = spans[REQUEST_BODY_EVENT] as Span? - val responseHeadersSpan = spans[RESPONSE_HEADERS_EVENT] as Span? - val responseBodySpan = spans[RESPONSE_BODY_EVENT] as Span? - assertNotNull(connectionSpan) - assertEquals(connectionSpan.spanId, requestHeadersSpan?.parentSpanId) - assertEquals(connectionSpan.spanId, requestBodySpan?.parentSpanId) - assertEquals(connectionSpan.spanId, responseHeadersSpan?.parentSpanId) - assertEquals(connectionSpan.spanId, responseBodySpan?.parentSpanId) - } - - @Test - fun `request and response spans are children of root span if connection span is not available`() { - val sut = fixture.getSut() - sut.startSpan(REQUEST_HEADERS_EVENT) - sut.startSpan(REQUEST_BODY_EVENT) - sut.startSpan(RESPONSE_HEADERS_EVENT) - sut.startSpan(RESPONSE_BODY_EVENT) - val spans = sut.getEventSpans() - val connectionSpan = spans[CONNECTION_EVENT] as Span? - val requestHeadersSpan = spans[REQUEST_HEADERS_EVENT] as Span? - val requestBodySpan = spans[REQUEST_BODY_EVENT] as Span? - val responseHeadersSpan = spans[RESPONSE_HEADERS_EVENT] as Span? - val responseBodySpan = spans[RESPONSE_BODY_EVENT] as Span? - val rootSpan = sut.callRootSpan as Span? - assertNotNull(rootSpan) - assertNull(connectionSpan) - assertEquals(rootSpan.spanId, requestHeadersSpan?.parentSpanId) - assertEquals(rootSpan.spanId, requestBodySpan?.parentSpanId) - assertEquals(rootSpan.spanId, responseHeadersSpan?.parentSpanId) - assertEquals(rootSpan.spanId, responseBodySpan?.parentSpanId) - } - - @Test - fun `finishSpan beforeFinish is called on span, parent and call root span`() { - val sut = fixture.getSut() - sut.startSpan(CONNECTION_EVENT) - sut.startSpan(REQUEST_HEADERS_EVENT) - sut.startSpan("random event") - sut.finishSpan(REQUEST_HEADERS_EVENT) { it.status = SpanStatus.INTERNAL_ERROR } - sut.finishSpan("random event") { it.status = SpanStatus.DEADLINE_EXCEEDED } - sut.finishSpan(CONNECTION_EVENT) - sut.finishEvent() - val spans = sut.getEventSpans() - val connectionSpan = spans[CONNECTION_EVENT] as Span? - val requestHeadersSpan = spans[REQUEST_HEADERS_EVENT] as Span? - val randomEventSpan = spans["random event"] as Span? - assertNotNull(connectionSpan) - assertNotNull(requestHeadersSpan) - assertNotNull(randomEventSpan) - // requestHeadersSpan was finished with INTERNAL_ERROR - assertEquals(SpanStatus.INTERNAL_ERROR, requestHeadersSpan.status) - // randomEventSpan was finished with DEADLINE_EXCEEDED - assertEquals(SpanStatus.DEADLINE_EXCEEDED, randomEventSpan.status) - // requestHeadersSpan was finished with INTERNAL_ERROR, and it propagates to its parent - assertEquals(SpanStatus.INTERNAL_ERROR, connectionSpan.status) - // random event was finished last with DEADLINE_EXCEEDED, and it propagates to root call - assertEquals(SpanStatus.DEADLINE_EXCEEDED, sut.callRootSpan!!.status) - } - - @Test - fun `finishEvent moves throwables from inner span to call root span`() { - val sut = fixture.getSut() - val throwable = RuntimeException() - sut.startSpan(CONNECTION_EVENT) - sut.startSpan("random event") - sut.finishSpan("random event") { it.status = SpanStatus.DEADLINE_EXCEEDED } - sut.finishSpan(CONNECTION_EVENT) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = throwable - } - sut.finishEvent() - val spans = sut.getEventSpans() - val connectionSpan = spans[CONNECTION_EVENT] as Span? - val randomEventSpan = spans["random event"] as Span? - assertNotNull(connectionSpan) - assertNotNull(randomEventSpan) - // randomEventSpan was finished with DEADLINE_EXCEEDED - assertEquals(SpanStatus.DEADLINE_EXCEEDED, randomEventSpan.status) - // connectionSpan was finished with INTERNAL_ERROR - assertEquals(SpanStatus.INTERNAL_ERROR, connectionSpan.status) - - // connectionSpan was finished last with INTERNAL_ERROR and a throwable, and it's moved to the root call - assertEquals(SpanStatus.INTERNAL_ERROR, sut.callRootSpan!!.status) - assertEquals(throwable, sut.callRootSpan.throwable) - assertNull(connectionSpan.throwable) - } - - @Test - fun `scheduleFinish schedules finishEvent and finish running spans to specific timestamp`() { - fixture.hub.options.executorService = ImmediateExecutorService() - val sut = spy(fixture.getSut()) - val timestamp = mock() - sut.startSpan(CONNECTION_EVENT) - sut.scheduleFinish(timestamp) - verify(sut).finishEvent(eq(timestamp), anyOrNull()) - val spans = sut.getEventSpans() - assertEquals(timestamp, spans[CONNECTION_EVENT]?.finishDate) - } - - @Test - fun `finishEvent with timestamp trims call root span`() { - val sut = fixture.getSut() - val timestamp = mock() - sut.finishEvent(finishDate = timestamp) - assertEquals(timestamp, sut.callRootSpan!!.finishDate) - } - - @Test - fun `scheduleFinish does not throw if executor is shut down`() { - val executorService = mock() - whenever(executorService.schedule(any(), any())).thenThrow(RejectedExecutionException()) - whenever(fixture.hub.options).thenReturn(SentryOptions().apply { this.executorService = executorService }) - val sut = fixture.getSut() - sut.scheduleFinish(mock()) - } - - @Test - fun `setClientErrorResponse will capture the client error on finishEvent`() { + fun `setClientErrorResponse will capture the client error on finish`() { val sut = fixture.getSut() val clientErrorResponse = mock() whenever(clientErrorResponse.request).thenReturn(fixture.mockRequest) sut.setClientErrorResponse(clientErrorResponse) - verify(fixture.hub, never()).captureEvent(any(), any()) - sut.finishEvent() - assertNotNull(sut.callRootSpan) - verify(fixture.hub).captureEvent( + verify(fixture.scopes, never()).captureEvent(any(), any()) + sut.finish() + assertNotNull(sut.callSpan) + verify(fixture.scopes).captureEvent( argThat { throwable is SentryHttpClientException && throwable!!.message!!.startsWith("HTTP Client Error with status code: ") @@ -583,15 +397,15 @@ class SentryOkHttpEventTest { } @Test - fun `setClientErrorResponse will capture the client error on finishEvent even when no span is running`() { + fun `setClientErrorResponse will capture the client error on finish even when no span is running`() { val sut = fixture.getSut(currentSpan = null) val clientErrorResponse = mock() whenever(clientErrorResponse.request).thenReturn(fixture.mockRequest) sut.setClientErrorResponse(clientErrorResponse) - verify(fixture.hub, never()).captureEvent(any(), any()) - sut.finishEvent() - assertNull(sut.callRootSpan) - verify(fixture.hub).captureEvent( + verify(fixture.scopes, never()).captureEvent(any(), any()) + sut.finish() + assertNull(sut.callSpan) + verify(fixture.scopes).captureEvent( argThat { throwable is SentryHttpClientException && throwable!!.message!!.startsWith("HTTP Client Error with status code: ") @@ -606,10 +420,10 @@ class SentryOkHttpEventTest { @Test fun `when setClientErrorResponse is not called, no client error is captured`() { val sut = fixture.getSut() - sut.finishEvent() - verify(fixture.hub, never()).captureEvent(any(), any()) + sut.finish() + verify(fixture.scopes, never()).captureEvent(any(), any()) } /** Retrieve all the spans started in the event using reflection. */ - private fun SentryOkHttpEvent.getEventSpans() = getProperty>("eventSpans") + private fun SentryOkHttpEvent.getEventDates() = getProperty>("eventDates") } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index fce16d92201..f18b9673337 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -6,18 +6,21 @@ import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.HttpStatusCodeRange -import io.sentry.IHub import io.sentry.IScope +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback +import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.SentryTraceHeader import io.sentry.SentryTracer +import io.sentry.Span import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext import io.sentry.TypeCheckHint import io.sentry.exception.SentryHttpClientException +import io.sentry.mockServerRequestTimeoutMillis import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -36,6 +39,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.IOException +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -47,7 +51,7 @@ import kotlin.test.fail class SentryOkHttpInterceptorTest { class Fixture { - val hub = mock() + val scopes = mock() val server = MockWebServer() lateinit var sentryTracer: SentryTracer lateinit var options: SentryOptions @@ -70,25 +74,27 @@ class SentryOkHttpInterceptorTest { HttpStatusCodeRange.DEFAULT_MAX ) ), - sendDefaultPii: Boolean = false + sendDefaultPii: Boolean = false, + optionsConfiguration: Sentry.OptionsConfiguration? = null ): OkHttpClient { - options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" + options = SentryOptions().also { + optionsConfiguration?.configure(it) + it.dsn = "https://key@sentry.io/proj" if (includeMockServerInTracePropagationTargets) { - setTracePropagationTargets(listOf(server.hostName)) + it.setTracePropagationTargets(listOf(server.hostName)) } else if (!keepDefaultTracePropagationTargets) { - setTracePropagationTargets(listOf("other-api")) + it.setTracePropagationTargets(listOf("other-api")) } - isSendDefaultPii = sendDefaultPii + it.isSendDefaultPii = sendDefaultPii } scope = Scope(options) - whenever(hub.options).thenReturn(options) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + whenever(scopes.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } server.enqueue( MockResponse() @@ -100,14 +106,14 @@ class SentryOkHttpInterceptorTest { val interceptor = when (captureFailedRequests) { null -> SentryOkHttpInterceptor( - hub, + scopes, beforeSpan, failedRequestTargets = failedRequestTargets, failedRequestStatusCodes = failedRequestStatusCodes ) else -> SentryOkHttpInterceptor( - hub, + scopes, beforeSpan, captureFailedRequests = captureFailedRequests, failedRequestTargets = failedRequestTargets, @@ -158,7 +164,7 @@ class SentryOkHttpInterceptorTest { fun `when there is an active span and server is listed in tracing origins, adds sentry trace headers to the request`() { val sut = fixture.getSut() sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -169,7 +175,7 @@ class SentryOkHttpInterceptorTest { val sut = fixture.getSut(keepDefaultTracePropagationTargets = true) sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -179,7 +185,7 @@ class SentryOkHttpInterceptorTest { fun `when there is an active span and server is not listed in tracing origins, does not add sentry trace headers to the request`() { val sut = fixture.getSut(includeMockServerInTracePropagationTargets = false) sut.newCall(Request.Builder().get().url(fixture.server.url("/hello")).build()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -190,7 +196,7 @@ class SentryOkHttpInterceptorTest { val sut = fixture.getSut() fixture.options.setTracePropagationTargets(emptyList()) sut.newCall(Request.Builder().get().url(fixture.server.url("/hello")).build()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -199,17 +205,28 @@ class SentryOkHttpInterceptorTest { fun `when there is no active span, adds sentry trace header to the request from scope`() { val sut = fixture.getSut(isSpanActive = false) sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + val sut = fixture.getSut(isSpanActive = false) { options -> + options.setIgnoredSpanOrigins(listOf("auto.http.okhttp")) + } + sut.newCall(getRequest()).execute() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + @Test fun `when there is no active span and host if not allowed, does not add sentry trace header to the request`() { val sut = fixture.getSut(isSpanActive = false) fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) sut.newCall(getRequest()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -218,7 +235,7 @@ class SentryOkHttpInterceptorTest { fun `when there is an active span, existing baggage headers are merged with sentry baggage into single header`() { val sut = fixture.getSut() sut.newCall(getRequestWithBaggageHeader()).execute() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) @@ -281,7 +298,7 @@ class SentryOkHttpInterceptorTest { fun `adds breadcrumb when http calls succeeds`() { val sut = fixture.getSut(responseBody = "response body") sut.newCall(postRequest()).execute() - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) assertEquals(13L, it.data[SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY]) @@ -296,7 +313,7 @@ class SentryOkHttpInterceptorTest { fun `adds breadcrumb when http calls results in exception`() { // to setup mocks fixture.getSut() - val interceptor = SentryOkHttpInterceptor(fixture.hub) + val interceptor = SentryOkHttpInterceptor(fixture.scopes) val chain = mock() whenever(chain.call()).thenReturn(mock()) whenever(chain.proceed(any())).thenThrow(IOException()) @@ -308,7 +325,7 @@ class SentryOkHttpInterceptorTest { } catch (e: IOException) { // ignore me } - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) }, @@ -385,7 +402,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test @@ -396,7 +413,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test @@ -406,7 +423,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -417,7 +434,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -429,7 +446,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -440,7 +457,7 @@ class SentryOkHttpInterceptorTest { ) sut.newCall(getRequest()).execute() - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), check { assertNotNull(it.get(TypeCheckHint.OKHTTP_REQUEST)) @@ -462,7 +479,7 @@ class SentryOkHttpInterceptorTest { val request = getRequest(url = "/hello?myQuery=myValue#myFragment") val response = sut.newCall(request).execute() - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val sentryRequest = it.request!! assertEquals("http://localhost:${fixture.server.port}/hello", sentryRequest.url) @@ -503,7 +520,7 @@ class SentryOkHttpInterceptorTest { sut.newCall(postRequest(body = body)).execute() - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val sentryRequest = it.request!! assertEquals(body.contentLength(), sentryRequest.bodySize) @@ -522,7 +539,7 @@ class SentryOkHttpInterceptorTest { sut.newCall(getRequest()).execute() - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( check { val sentryRequest = it.request!! assertEquals("myValue", sentryRequest.headers!!["myHeader"]) @@ -540,7 +557,7 @@ class SentryOkHttpInterceptorTest { // to setup mocks fixture.getSut() val interceptor = SentryOkHttpInterceptor( - fixture.hub, + fixture.scopes, captureFailedRequests = true ) val chain = mock() @@ -554,7 +571,7 @@ class SentryOkHttpInterceptorTest { } catch (e: IOException) { // ignore me } - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) } @Test @@ -565,7 +582,7 @@ class SentryOkHttpInterceptorTest { call.execute() val httpClientSpan = fixture.sentryTracer.children.firstOrNull() assertNull(httpClientSpan) - verify(fixture.hub, never()).addBreadcrumb(any(), anyOrNull()) + verify(fixture.scopes, never()).addBreadcrumb(any(), anyOrNull()) } @Test @@ -573,7 +590,7 @@ class SentryOkHttpInterceptorTest { val sut = fixture.getSut(captureFailedRequests = true, httpStatusCode = 500) val call = sut.newCall(getRequest()) call.execute() - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test @@ -582,6 +599,18 @@ class SentryOkHttpInterceptorTest { val call = sut.newCall(getRequest()) SentryOkHttpEventListener.eventMap[call] = mock() call.execute() - verify(fixture.hub, never()).captureEvent(any(), any()) + verify(fixture.scopes, never()).captureEvent(any(), any()) + } + + @Test + fun `when a call is captured by SentryOkHttpEventListener, interceptor finishes event`() { + val sut = fixture.getSut() + val call = sut.newCall(getRequest()) + val event = mock() + val span = Span(mock(), fixture.sentryTracer, fixture.scopes, mock()) + whenever(event.callSpan).thenReturn(span) + SentryOkHttpEventListener.eventMap[call] = event + call.execute() + verify(event).finish() } } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt index ec194543271..c7194e59946 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt @@ -1,7 +1,7 @@ package io.sentry.okhttp import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.TransactionContext @@ -29,7 +29,7 @@ import kotlin.test.assertTrue class SentryOkHttpUtilsTest { class Fixture { - val hub = mock() + val scopes = mock() val server = MockWebServer() fun getSut( @@ -43,11 +43,11 @@ class SentryOkHttpUtilsTest { setTracePropagationTargets(listOf(server.hostName)) isSendDefaultPii = sendDefaultPii } - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) - val sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) server.enqueue( MockResponse() @@ -78,8 +78,8 @@ class SentryOkHttpUtilsTest { val request = getRequest() val response = sut.newCall(request).execute() - SentryOkHttpUtils.captureClientError(fixture.hub, request, response) - verify(fixture.hub).captureEvent( + SentryOkHttpUtils.captureClientError(fixture.scopes, request, response) + verify(fixture.scopes).captureEvent( check { val req = it.request val resp = it.contexts.response @@ -103,8 +103,8 @@ class SentryOkHttpUtilsTest { val request = getRequest() val response = sut.newCall(request).execute() - SentryOkHttpUtils.captureClientError(fixture.hub, request, response) - verify(fixture.hub).captureEvent( + SentryOkHttpUtils.captureClientError(fixture.scopes, request, response) + verify(fixture.scopes).captureEvent( check { val req = it.request val resp = it.contexts.response @@ -127,8 +127,8 @@ class SentryOkHttpUtilsTest { val request = getRequest() val response = sut.newCall(request).execute() - SentryOkHttpUtils.captureClientError(fixture.hub, request, response) - verify(fixture.hub).captureEvent( + SentryOkHttpUtils.captureClientError(fixture.scopes, request, response) + verify(fixture.scopes).captureEvent( check { val req = it.request val resp = it.contexts.response diff --git a/sentry-openfeign/api/sentry-openfeign.api b/sentry-openfeign/api/sentry-openfeign.api index beb15c9e028..4ab65a5ca4d 100644 --- a/sentry-openfeign/api/sentry-openfeign.api +++ b/sentry-openfeign/api/sentry-openfeign.api @@ -1,12 +1,12 @@ public final class io/sentry/openfeign/SentryCapability : feign/Capability { public fun ()V - public fun (Lio/sentry/IHub;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V + public fun (Lio/sentry/IScopes;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V public fun (Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V public fun enrich (Lfeign/Client;)Lfeign/Client; } public final class io/sentry/openfeign/SentryFeignClient : feign/Client { - public fun (Lfeign/Client;Lio/sentry/IHub;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V + public fun (Lfeign/Client;Lio/sentry/IScopes;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V public fun execute (Lfeign/Request;Lfeign/Request$Options;)Lfeign/Response; } diff --git a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryCapability.java b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryCapability.java index b65685c3fd5..1ad6b1f2742 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryCapability.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryCapability.java @@ -2,33 +2,34 @@ import feign.Capability; import feign.Client; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Adds Sentry tracing capability to Feign clients. */ public final class SentryCapability implements Capability { - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan; public SentryCapability( - final @NotNull IHub hub, final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan) { - this.hub = hub; + final @NotNull IScopes scopes, + final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan) { + this.scopes = scopes; this.beforeSpan = beforeSpan; } public SentryCapability(final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan) { - this(HubAdapter.getInstance(), beforeSpan); + this(ScopesAdapter.getInstance(), beforeSpan); } public SentryCapability() { - this(HubAdapter.getInstance(), null); + this(ScopesAdapter.getInstance(), null); } @Override public @NotNull Client enrich(final @NotNull Client client) { - return new SentryFeignClient(client, hub, beforeSpan); + return new SentryFeignClient(client, scopes, beforeSpan); } } 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 cb8aa3d9e08..935c4229ab1 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -9,11 +9,13 @@ import io.sentry.BaggageHeader; import io.sentry.Breadcrumb; import io.sentry.Hint; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; import io.sentry.SpanStatus; import io.sentry.util.Objects; +import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; import io.sentry.util.UrlUtils; import java.io.IOException; @@ -30,15 +32,15 @@ public final class SentryFeignClient implements Client { private static final String TRACE_ORIGIN = "auto.http.openfeign"; private final @NotNull Client delegate; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private final @Nullable BeforeSpanCallback beforeSpan; public SentryFeignClient( final @NotNull Client delegate, - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @Nullable BeforeSpanCallback beforeSpan) { this.delegate = Objects.requireNonNull(delegate, "delegate is required"); - this.hub = Objects.requireNonNull(hub, "hub is required"); + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); this.beforeSpan = beforeSpan; } @@ -47,15 +49,16 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O throws IOException { Response response = null; try { - final ISpan activeSpan = hub.getSpan(); + final ISpan activeSpan = scopes.getSpan(); if (activeSpan == null) { final @NotNull Request modifiedRequest = maybeAddTracingHeaders(request, null); return delegate.execute(modifiedRequest, options); } - ISpan span = activeSpan.startChild("http.client"); - span.getSpanContext().setOrigin(TRACE_ORIGIN); + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + ISpan span = activeSpan.startChild("http.client", null, spanOptions); final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url()); final @NotNull String method = request.httpMethod().name(); span.setDescription(method + " " + urlDetails.getUrlOrFallback()); @@ -96,13 +99,17 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O private @NotNull Request maybeAddTracingHeaders( final @NotNull Request request, final @Nullable ISpan span) { + if (isIgnored()) { + return request; + } + final @NotNull RequestWrapper requestWrapper = new RequestWrapper(request); final @Nullable Collection requestBaggageHeaders = request.headers().get(BaggageHeader.BAGGAGE_HEADER); final @Nullable TracingUtils.TracingHeaders tracingHeaders = TracingUtils.traceIfAllowed( - hub, + scopes, request.url(), (requestBaggageHeaders != null ? new ArrayList<>(requestBaggageHeaders) : null), span); @@ -122,6 +129,10 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O return requestWrapper.build(); } + private boolean isIgnored() { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN); + } + private void addBreadcrumb(final @NotNull Request request, final @Nullable Response response) { final Breadcrumb breadcrumb = Breadcrumb.http( @@ -139,7 +150,7 @@ private void addBreadcrumb(final @NotNull Request request, final @Nullable Respo hint.set(OPEN_FEIGN_RESPONSE, response); } - hub.addBreadcrumb(breadcrumb, hint); + scopes.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 65e56ab02bc..539abbf70bd 100644 --- a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt +++ b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt @@ -7,7 +7,7 @@ import feign.HeaderMap import feign.RequestLine import io.sentry.BaggageHeader import io.sentry.Breadcrumb -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -16,6 +16,7 @@ import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext +import io.sentry.mockServerRequestTimeoutMillis import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.mockito.kotlin.any @@ -25,6 +26,7 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -37,7 +39,7 @@ import kotlin.test.fail class SentryFeignClientTest { class Fixture { - val hub = mock() + val scopes = mock() val server = MockWebServer() val sentryTracer: SentryTracer val sentryOptions = SentryOptions().apply { @@ -46,9 +48,9 @@ class SentryFeignClientTest { val scope = Scope(sentryOptions) init { - whenever(hub.options).thenReturn(sentryOptions) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + whenever(scopes.options).thenReturn(sentryOptions) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) } fun getSut( @@ -59,7 +61,7 @@ class SentryFeignClientTest { beforeSpan: SentryFeignClient.BeforeSpanCallback? = null ): MockApi { if (isSpanActive) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } server.enqueue( MockResponse() @@ -70,12 +72,12 @@ class SentryFeignClientTest { return if (!networkError) { Feign.builder() - .addCapability(SentryCapability(hub, beforeSpan)) + .addCapability(SentryCapability(scopes, beforeSpan)) } else { val mockClient = mock() whenever(mockClient.execute(any(), any())).thenThrow(RuntimeException::class.java) Feign.builder() - .client(SentryFeignClient(mockClient, hub, beforeSpan)) + .client(SentryFeignClient(mockClient, scopes, beforeSpan)) }.target(MockApi::class.java, server.url("/").toUrl().toString()) } } @@ -93,7 +95,7 @@ class SentryFeignClientTest { fixture.sentryOptions.dsn = "https://key@sentry.io/proj" val sut = fixture.getSut() sut.getOk() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -106,7 +108,7 @@ class SentryFeignClientTest { sut.getOkWithBaggageHeader(mapOf("baggage" to listOf("thirdPartyBaggage=someValue", "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue"))) - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) @@ -123,18 +125,29 @@ class SentryFeignClientTest { fixture.sentryOptions.dsn = "https://key@sentry.io/proj" val sut = fixture.getSut(isSpanActive = false) sut.getOk() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } + @Test + fun `does not add sentry trace header when span origin is ignored`() { + fixture.sentryOptions.dsn = "https://key@sentry.io/proj" + fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.openfeign")) + val sut = fixture.getSut(isSpanActive = false) + sut.getOk() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + @Test fun `when there is no active span, does not add sentry trace header to the request if host is disallowed`() { fixture.sentryOptions.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) fixture.sentryOptions.dsn = "https://key@sentry.io/proj" val sut = fixture.getSut(isSpanActive = false) sut.getOk() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -146,7 +159,7 @@ class SentryFeignClientTest { fixture.sentryOptions.dsn = "https://key@sentry.io/proj" val sut = fixture.getSut() sut.getOk() - val recorderRequest = fixture.server.takeRequest() + val recorderRequest = fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) } @@ -201,7 +214,7 @@ class SentryFeignClientTest { fun `adds breadcrumb when http calls succeeds`() { val sut = fixture.getSut(responseBody = "response body") sut.postWithBody("request-body") - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) assertEquals(13, it.data["response_body_size"]) @@ -215,7 +228,7 @@ class SentryFeignClientTest { fun `adds breadcrumb when http calls succeeds even though response body is null`() { val sut = fixture.getSut(responseBody = "") sut.postWithBody("request-body") - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) assertEquals(0, it.data["response_body_size"]) @@ -236,7 +249,7 @@ class SentryFeignClientTest { } catch (e: Exception) { // ignore me } - verify(fixture.hub).addBreadcrumb( + verify(fixture.scopes).addBreadcrumb( check { assertEquals("http", it.type) }, diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md index 76a118dbb21..02c5e3f1aed 100644 --- a/sentry-opentelemetry/README.md +++ b/sentry-opentelemetry/README.md @@ -22,9 +22,21 @@ application. Please see the module [README](sentry-opentelemetry-agent/README.md This contains customizations to the OpenTelemetry Java Agent such as registering the `SentrySpanProcessor` and `SentryPropagator` as well as providing default properties that enable the `sentry` propagator and disable exporters so our agent doesn't trigger lots of log -warnings due to OTLP server not being there. +warnings due to OTLP server not being there. This can also be used without the agent. + +### `sentry-opentelemetry-bootstrap` + +Classes that are loaded into the bootstrap classloader +(represented as `null` when invoking X.class.classLoader) +These are shared between the agent and the application and include things like storage, +utils, factory, tokens etc. + +If you want to use Sentry with OpenTelemetry without the agent, +you also need this module as a dependency. ### `sentry-opentelemetry-core` Contains `SentrySpanProcessor` and `SentryPropagator` which are used by our Java Agent but can also -be used when manually instrumenting using OpenTelemetry. +be used when manually instrumenting using OpenTelemetry. If you want to use OpenTelemetry without +the agent but still want some configuration convenience, you should rather use the +`sentry-opentelemetry-agentcustomization` module. diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts index 80b68430db2..2d4ea259c61 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts @@ -53,6 +53,7 @@ val upstreamAgent = configurations.create("upstreamAgent") { dependencies { bootstrapLibs(projects.sentry) + bootstrapLibs(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) javaagentLibs(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) upstreamAgent(Config.Libs.OpenTelemetry.otelJavaAgent) } @@ -150,11 +151,11 @@ tasks { attributes.put("Can-Redefine-Classes", "true") attributes.put("Can-Retransform-Classes", "true") attributes.put("Implementation-Vendor", "Sentry") - attributes.put("Implementation-Version", "sentry-${project.version}-otel-${Config.Libs.OpenTelemetry.otelJavaagentVersion}") + attributes.put("Implementation-Version", "sentry-${project.version}-otel-${Config.Libs.OpenTelemetry.otelInstrumentationVersion}") attributes.put("Sentry-Version-Name", project.version) attributes.put("Sentry-Opentelemetry-SDK-Name", Config.Sentry.SENTRY_OPENTELEMETRY_AGENT_SDK_NAME) attributes.put("Sentry-Opentelemetry-Version-Name", Config.Libs.OpenTelemetry.otelVersion) - attributes.put("Sentry-Opentelemetry-Javaagent-Version-Name", Config.Libs.OpenTelemetry.otelJavaagentVersion) + attributes.put("Sentry-Opentelemetry-Javaagent-Version-Name", Config.Libs.OpenTelemetry.otelInstrumentationVersion) } } @@ -162,3 +163,7 @@ tasks { dependsOn(shadowJar) } } + +tasks.named("distZip").configure { + this.dependsOn("jar") +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/src/main/java/io/sentry/opentelemetry/agent/AgentMarker.java b/sentry-opentelemetry/sentry-opentelemetry-agent/src/main/java/io/sentry/opentelemetry/agent/AgentMarker.java new file mode 100644 index 00000000000..5b58006c70c --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/src/main/java/io/sentry/opentelemetry/agent/AgentMarker.java @@ -0,0 +1,7 @@ +package io.sentry.opentelemetry.agent; + +/** + * Marker class used to check if the Sentry Java Agent is active. If so, this class should be on the + * classpath. + */ +public final class AgentMarker {} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api index 342f71b5bb1..c35b221aa07 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api @@ -1,4 +1,5 @@ public final class io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider : io/opentelemetry/sdk/autoconfigure/spi/AutoConfigurationCustomizerProvider { + public static field skipInit Z public fun ()V public fun customize (Lio/opentelemetry/sdk/autoconfigure/spi/AutoConfigurationCustomizer;)V } diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts index 79e3599cc8e..77ef8f56a84 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { exclude(group = "io.opentelemetry") exclude(group = "io.opentelemetry.javaagent") } + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) compileOnly(Config.Libs.OpenTelemetry.otelSdk) compileOnly(Config.Libs.OpenTelemetry.otelExtensionAutoconfigureSpi) diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java index e808db8fcf0..37851c14550 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java @@ -4,7 +4,8 @@ import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; -import io.sentry.Instrumenter; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.sentry.InitPriority; import io.sentry.Sentry; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryOptions; @@ -25,15 +26,18 @@ public final class SentryAutoConfigurationCustomizerProvider implements AutoConfigurationCustomizerProvider { + public static volatile boolean skipInit = false; + @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { + ensureSentryOtelStorageIsInitialized(); final @Nullable VersionInfoHolder versionInfoHolder = createVersionInfo(); + if (isSentryAutoInitEnabled()) { Sentry.init( options -> { options.setEnableExternalConfiguration(true); - options.setInstrumenter(Instrumenter.OTEL); - options.addEventProcessor(new OpenTelemetryLinkErrorEventProcessor()); + options.setInitPriority(InitPriority.HIGH); final @Nullable SdkVersion sdkVersion = createSdkVersion(options, versionInfoHolder); if (sdkVersion != null) { options.setSdkVersion(sdkVersion); @@ -55,7 +59,19 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { .addPropertiesSupplier(this::getDefaultProperties); } + private static void ensureSentryOtelStorageIsInitialized() { + /* + accessing Sentry.something will cause ScopesStorageFactory to run, + which causes OtelContextScopesStorage.init to register SentryContextStorage + as a wrapper. The wrapper can only be set until storage has been initialized by OpenTelemetry. + */ + Sentry.getGlobalScope(); + } + private boolean isSentryAutoInitEnabled() { + if (skipInit) { + return false; + } final @Nullable String sentryAutoInit = System.getenv("SENTRY_AUTO_INIT"); if (sentryAutoInit != null) { @@ -140,7 +156,10 @@ private static class VersionInfoHolder { private SdkTracerProviderBuilder configureSdkTracerProvider( SdkTracerProviderBuilder tracerProvider, ConfigProperties config) { - return tracerProvider.addSpanProcessor(new SentrySpanProcessor()); + return tracerProvider + .setSampler(new SentrySampler()) + .addSpanProcessor(new OtelSentrySpanProcessor()) + .addSpanProcessor(BatchSpanProcessor.builder(new SentrySpanExporter()).build()); } private Map getDefaultProperties() { diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java index 49acd725fb7..6aa04f31e9f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java @@ -7,7 +7,7 @@ public final class SentryPropagatorProvider implements ConfigurablePropagatorProvider { @Override public TextMapPropagator getPropagator(ConfigProperties config) { - return new SentryPropagator(); + return new OtelSentryPropagator(); } @Override diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/README.md b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/README.md new file mode 100644 index 00000000000..b808195c906 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/README.md @@ -0,0 +1,54 @@ +# sentry-opentelemetry-agentless-spring + +*NOTE: Our OpenTelemetry modules are still experimental. Any feedback is welcome.* + +## How to use it + +Add the latest `sentry-opentelemetry-agentless-spring` module as a dependency and add a `sentry.properties` +configuration file to your project that could look like this: + +```properties +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +traces-sample-rate=1.0 +``` + +For more details on configuring Sentry via `sentry.properties` please see the +[docs page](https://docs.sentry.io/platforms/java/configuration/). + +As an alternative to the `SENTRY_PROPERTIES_FILE` environment variable you can provide individual +settings as environment variables (e.g. `SENTRY_DSN=...`) or you may initialize `Sentry` inside +your target application. If you do so, please make sure to apply OpenTelemetry specific options, e.g. +like this: + +``` +Sentry.init( + options -> { + options.setDsn("..."); + ... + OpenTelemetryUtil.applyOpenTelemetryOptions(options, false); + } +) +``` + +## Getting rid of exporter error messages + +In case you are using this module without needing to use any OpenTelemetry exporters you can add +the following environment variables to turn off exporters and stop seeing error messages about +servers not being reachable in the logs. + +Example log message: +``` +ERROR io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter - Failed to export spans. The request could not be executed. Full error message: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:4317 +ERROR io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter - Failed to export metrics. The request could not be executed. Full error message: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:4317 +``` + +### Traces + +To turn off exporting of traces you can set `OTEL_TRACES_EXPORTER=none` +see [OpenTelemetry GitHub](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#otlp-exporter-span-metric-and-log-exporters) + +### Metrics + +To turn off exporting of metrics you can set `OTEL_METRICS_EXPORTER=none` +see [OpenTelemetry GitHub](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#otlp-exporter-span-metric-and-log-exporters) diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts new file mode 100644 index 00000000000..a79bd6c94f7 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api(projects.sentry) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + api(Config.Libs.OpenTelemetry.otelSdk) + api(Config.Libs.OpenTelemetry.otelSemconv) + api(Config.Libs.OpenTelemetry.otelSemconvIncubating) + api(Config.Libs.OpenTelemetry.otelExtensionAutoconfigure) + api(Config.Libs.springBoot3StarterOpenTelemetry) +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/src/main/java/io/sentry/opentelemetry/agent/AgentlessSpringMarker.java b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/src/main/java/io/sentry/opentelemetry/agent/AgentlessSpringMarker.java new file mode 100644 index 00000000000..eb9441148fc --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless-spring/src/main/java/io/sentry/opentelemetry/agent/AgentlessSpringMarker.java @@ -0,0 +1,3 @@ +package io.sentry.opentelemetry.agent; + +public final class AgentlessSpringMarker {} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless/README.md b/sentry-opentelemetry/sentry-opentelemetry-agentless/README.md new file mode 100644 index 00000000000..9ce3319baef --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless/README.md @@ -0,0 +1,54 @@ +# sentry-opentelemetry-agentless + +*NOTE: Our OpenTelemetry modules are still experimental. Any feedback is welcome.* + +## How to use it + +Add the latest `sentry-opentelemetry-agentless` module as a dependency and add a `sentry.properties` +configuration file to your project that could look like this: + +```properties +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +traces-sample-rate=1.0 +``` + +For more details on configuring Sentry via `sentry.properties` please see the +[docs page](https://docs.sentry.io/platforms/java/configuration/). + +As an alternative to the `SENTRY_PROPERTIES_FILE` environment variable you can provide individual +settings as environment variables (e.g. `SENTRY_DSN=...`) or you may initialize `Sentry` inside +your target application. If you do so, please make sure to apply OpenTelemetry specific options, e.g. +like this: + +``` +Sentry.init( + options -> { + options.setDsn("..."); + ... + OpenTelemetryUtil.applyOpenTelemetryOptions(options, false); + } +) +``` + +## Getting rid of exporter error messages + +In case you are using this module without needing to use any OpenTelemetry exporters you can add +the following environment variables to turn off exporters and stop seeing error messages about +servers not being reachable in the logs. + +Example log message: +``` +ERROR io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter - Failed to export spans. The request could not be executed. Full error message: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:4317 +ERROR io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter - Failed to export metrics. The request could not be executed. Full error message: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:4317 +``` + +### Traces + +To turn off exporting of traces you can set `OTEL_TRACES_EXPORTER=none` +see [OpenTelemetry GitHub](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#otlp-exporter-span-metric-and-log-exporters) + +### Metrics + +To turn off exporting of metrics you can set `OTEL_METRICS_EXPORTER=none` +see [OpenTelemetry GitHub](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#otlp-exporter-span-metric-and-log-exporters) diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts new file mode 100644 index 00000000000..26a404e49c9 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-library` +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api(projects.sentry) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + api(Config.Libs.OpenTelemetry.otelSdk) + api(Config.Libs.OpenTelemetry.otelSemconv) + api(Config.Libs.OpenTelemetry.otelSemconvIncubating) + api(Config.Libs.OpenTelemetry.otelExtensionAutoconfigure) +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentless/src/main/java/io/sentry/opentelemetry/agent/AgentlessMarker.java b/sentry-opentelemetry/sentry-opentelemetry-agentless/src/main/java/io/sentry/opentelemetry/agent/AgentlessMarker.java new file mode 100644 index 00000000000..ddb5101c29b --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentless/src/main/java/io/sentry/opentelemetry/agent/AgentlessMarker.java @@ -0,0 +1,3 @@ +package io.sentry.opentelemetry.agent; + +public final class AgentlessMarker {} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api new file mode 100644 index 00000000000..df7db47c652 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -0,0 +1,183 @@ +public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/sentry/ISpan { + public abstract fun getData ()Ljava/util/Map; + public abstract fun getMeasurements ()Ljava/util/Map; + public abstract fun getScopes ()Lio/sentry/IScopes; + public abstract fun getTags ()Ljava/util/Map; + public abstract fun getTraceId ()Lio/sentry/protocol/SentryId; + public abstract fun getTransactionName ()Ljava/lang/String; + public abstract fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public abstract fun isProfileSampled ()Ljava/lang/Boolean; + public abstract fun setTransactionName (Ljava/lang/String;)V + public abstract fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public abstract fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; +} + +public final class io/sentry/opentelemetry/InternalSemanticAttributes { + public static final field BAGGAGE Lio/opentelemetry/api/common/AttributeKey; + public static final field BAGGAGE_MUTABLE Lio/opentelemetry/api/common/AttributeKey; + public static final field IS_REMOTE_PARENT Lio/opentelemetry/api/common/AttributeKey; + public static final field PARENT_SAMPLED Lio/opentelemetry/api/common/AttributeKey; + public static final field PROFILE_SAMPLED Lio/opentelemetry/api/common/AttributeKey; + public static final field PROFILE_SAMPLE_RATE Lio/opentelemetry/api/common/AttributeKey; + public static final field SAMPLED Lio/opentelemetry/api/common/AttributeKey; + public static final field SAMPLE_RATE Lio/opentelemetry/api/common/AttributeKey; + public fun ()V +} + +public final class io/sentry/opentelemetry/OtelContextScopesStorage : io/sentry/IScopesStorage { + public fun ()V + public fun close ()V + public fun get ()Lio/sentry/IScopes; + public fun init ()V + public fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken; +} + +public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFactory { + public fun ()V + public fun (Lio/opentelemetry/api/OpenTelemetry;)V + public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; +} + +public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { + public fun (Lio/opentelemetry/api/trace/Span;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V + public fun finish ()V + public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V + public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getData ()Ljava/util/Map; + public fun getData (Ljava/lang/String;)Ljava/lang/Object; + public fun getDescription ()Ljava/lang/String; + public fun getFinishDate ()Lio/sentry/SentryDate; + public fun getMeasurements ()Ljava/util/Map; + public fun getOperation ()Ljava/lang/String; + public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; + public fun getScopes ()Lio/sentry/IScopes; + public fun getSpanContext ()Lio/sentry/SpanContext; + public fun getStartDate ()Lio/sentry/SentryDate; + public fun getStatus ()Lio/sentry/SpanStatus; + public fun getTag (Ljava/lang/String;)Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public fun getThrowable ()Ljava/lang/Throwable; + public fun getTraceId ()Lio/sentry/protocol/SentryId; + public fun getTransactionName ()Ljava/lang/String; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public fun isFinished ()Z + public fun isNoOp ()Z + public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isSampled ()Ljava/lang/Boolean; + public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V + public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setOperation (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/SpanStatus;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setThrowable (Ljava/lang/Throwable;)V + public fun setTransactionName (Ljava/lang/String;)V + public fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; + public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; + public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; + public fun traceContext ()Lio/sentry/TraceContext; + public fun updateEndDate (Lio/sentry/SentryDate;)Z +} + +public final class io/sentry/opentelemetry/OtelTransactionSpanForwarder : io/sentry/ITransaction { + public fun (Lio/sentry/opentelemetry/IOtelSpanWrapper;)V + public fun finish ()V + public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V + public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getData (Ljava/lang/String;)Ljava/lang/Object; + public fun getDescription ()Ljava/lang/String; + public fun getEventId ()Lio/sentry/protocol/SentryId; + public fun getFinishDate ()Lio/sentry/SentryDate; + public fun getLatestActiveSpan ()Lio/sentry/ISpan; + public fun getName ()Ljava/lang/String; + public fun getOperation ()Ljava/lang/String; + public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; + public fun getSpanContext ()Lio/sentry/SpanContext; + public fun getSpans ()Ljava/util/List; + public fun getStartDate ()Lio/sentry/SentryDate; + public fun getStatus ()Lio/sentry/SpanStatus; + public fun getTag (Ljava/lang/String;)Ljava/lang/String; + public fun getThrowable ()Ljava/lang/Throwable; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public fun isFinished ()Z + public fun isNoOp ()Z + public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isSampled ()Ljava/lang/Boolean; + public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun scheduleFinish ()V + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V + public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setName (Ljava/lang/String;)V + public fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun setOperation (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/SpanStatus;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setThrowable (Ljava/lang/Throwable;)V + public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; + public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; + public fun traceContext ()Lio/sentry/TraceContext; + public fun updateEndDate (Lio/sentry/SentryDate;)Z +} + +public final class io/sentry/opentelemetry/SentryContextStorage : io/opentelemetry/context/ContextStorage { + public fun (Lio/opentelemetry/context/ContextStorage;)V + public fun attach (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Scope; + public fun current ()Lio/opentelemetry/context/Context; +} + +public final class io/sentry/opentelemetry/SentryContextStorageProvider : io/opentelemetry/context/ContextStorageProvider { + public fun ()V + public fun get ()Lio/opentelemetry/context/ContextStorage; +} + +public final class io/sentry/opentelemetry/SentryContextWrapper : io/opentelemetry/context/Context { + public fun get (Lio/opentelemetry/context/ContextKey;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; + public fun with (Lio/opentelemetry/context/ContextKey;Ljava/lang/Object;)Lio/opentelemetry/context/Context; + public static fun wrap (Lio/opentelemetry/context/Context;)Lio/sentry/opentelemetry/SentryContextWrapper; +} + +public final class io/sentry/opentelemetry/SentryOtelKeys { + public static final field SENTRY_BAGGAGE_KEY Lio/opentelemetry/context/ContextKey; + public static final field SENTRY_SCOPES_KEY Lio/opentelemetry/context/ContextKey; + public static final field SENTRY_TRACE_KEY Lio/opentelemetry/context/ContextKey; + public fun ()V +} + +public final class io/sentry/opentelemetry/SentryOtelThreadLocalStorage : io/opentelemetry/context/ContextStorage { + public fun ()V + public fun attach (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Scope; + public fun current ()Lio/opentelemetry/context/Context; +} + +public final class io/sentry/opentelemetry/SentryWeakSpanStorage { + public static fun getInstance ()Lio/sentry/opentelemetry/SentryWeakSpanStorage; + public fun getSentrySpan (Lio/opentelemetry/api/trace/SpanContext;)Lio/sentry/opentelemetry/IOtelSpanWrapper; + public fun storeSentrySpan (Lio/opentelemetry/api/trace/SpanContext;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V +} + diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts new file mode 100644 index 00000000000..6eb8e6d6f16 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts @@ -0,0 +1,78 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +dependencies { + compileOnly(projects.sentry) + + compileOnly(Config.Libs.OpenTelemetry.otelSdk) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + errorprone(Config.CompileOnly.errorProneNullAway) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.awaitility) + + testImplementation(Config.Libs.OpenTelemetry.otelSdk) + testImplementation(Config.Libs.OpenTelemetry.otelSemconv) + testImplementation(Config.Libs.OpenTelemetry.otelSemconvIncubating) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java new file mode 100644 index 00000000000..0184db0eabe --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java @@ -0,0 +1,50 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.Context; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.protocol.MeasurementValue; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface IOtelSpanWrapper extends ISpan { + + void setTransactionName(@NotNull String name); + + void setTransactionName(@NotNull String name, @NotNull TransactionNameSource nameSource); + + @ApiStatus.Internal + @Nullable + TransactionNameSource getTransactionNameSource(); + + @ApiStatus.Internal + @Nullable + String getTransactionName(); + + @NotNull + SentryId getTraceId(); + + @NotNull + Map getData(); + + @NotNull + Map getMeasurements(); + + @Nullable + Boolean isProfileSampled(); + + @ApiStatus.Internal + @NotNull + IScopes getScopes(); + + @ApiStatus.Internal + @NotNull + Map getTags(); + + @NotNull + Context storeInContext(Context context); +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java new file mode 100644 index 00000000000..cb64d7bfffd --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java @@ -0,0 +1,22 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.AttributeKey; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class InternalSemanticAttributes { + public static final AttributeKey SAMPLED = AttributeKey.booleanKey("sentry.sampled"); + public static final AttributeKey SAMPLE_RATE = + AttributeKey.doubleKey("sentry.sample_rate"); + public static final AttributeKey PARENT_SAMPLED = + AttributeKey.booleanKey("sentry.parent_sampled"); + public static final AttributeKey PROFILE_SAMPLED = + AttributeKey.booleanKey("sentry.profile_sampled"); + public static final AttributeKey PROFILE_SAMPLE_RATE = + AttributeKey.doubleKey("sentry.profile_sample_rate"); + public static final AttributeKey IS_REMOTE_PARENT = + AttributeKey.booleanKey("sentry.is_remote_parent"); + public static final AttributeKey BAGGAGE = AttributeKey.stringKey("sentry.baggage"); + public static final AttributeKey BAGGAGE_MUTABLE = + AttributeKey.booleanKey("sentry.baggage_mutable"); +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelContextScopesStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelContextScopesStorage.java new file mode 100644 index 00000000000..143ebb6c16d --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelContextScopesStorage.java @@ -0,0 +1,48 @@ +package io.sentry.opentelemetry; + +import static io.sentry.opentelemetry.SentryOtelKeys.SENTRY_SCOPES_KEY; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.sentry.IScopes; +import io.sentry.IScopesStorage; +import io.sentry.ISentryLifecycleToken; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +@SuppressWarnings("MustBeClosedChecker") +public final class OtelContextScopesStorage implements IScopesStorage { + + @Override + public void init() { + /** + * We're currently overriding the storage mechanism to allow for cleanup of non closed OTel + * scopes. These happen when using e.g. Sentry static API due to getCurrentScopes() invoking + * Context.makeCurrent and then ignoring the returned lifecycle token (OTel Scope). After fixing + * the classloader problem (sentry bootstrap dependency is currently in agent classloader) we + * can revisit and try again to set the storage instead of overriding it in the wrapper. We + * should try to use OTels StorageProvider mechanism instead. + */ + // ContextStorage.addWrapper((storage) -> new SentryContextStorage(storage)); + // ContextStorage.addWrapper( + // (storage) -> new SentryContextStorage(new SentryOtelThreadLocalStorage())); + } + + @Override + public @NotNull ISentryLifecycleToken set(@Nullable IScopes scopes) { + final Context context = Context.current(); + final @NotNull Scope otelScope = context.with(SENTRY_SCOPES_KEY, scopes).makeCurrent(); + return new OtelStorageToken(otelScope); + } + + @Override + public @Nullable IScopes get() { + final Context context = Context.current(); + return context.get(SENTRY_SCOPES_KEY); + } + + @Override + public void close() {} +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java new file mode 100644 index 00000000000..547463dcdca --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -0,0 +1,178 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import io.sentry.Baggage; +import io.sentry.BuildConfig; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.ISpanFactory; +import io.sentry.ITransaction; +import io.sentry.NoOpSpan; +import io.sentry.NoOpTransaction; +import io.sentry.SentryDate; +import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanOptions; +import io.sentry.TracesSamplingDecision; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.TransactionPerformanceCollector; +import io.sentry.protocol.SentryId; +import io.sentry.util.SpanUtils; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelSpanFactory implements ISpanFactory { + + private final @NotNull SentryWeakSpanStorage storage = SentryWeakSpanStorage.getInstance(); + private final @Nullable OpenTelemetry openTelemetry; + + public OtelSpanFactory(final @Nullable OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public OtelSpanFactory() { + this(null); + } + + @Override + public @NotNull ITransaction createTransaction( + @NotNull TransactionContext context, + @NotNull IScopes scopes, + @NotNull TransactionOptions transactionOptions, + @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + final @Nullable IOtelSpanWrapper span = + createSpanInternal( + scopes, transactionOptions, null, context.getSamplingDecision(), context); + if (span == null) { + return NoOpTransaction.getInstance(); + } + return new OtelTransactionSpanForwarder(span); + } + + @Override + public @NotNull ISpan createSpan( + final @NotNull IScopes scopes, + final @NotNull SpanOptions spanOptions, + final @NotNull SpanContext spanContext, + final @Nullable ISpan parentSpan) { + if (SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), spanOptions.getOrigin())) { + return NoOpSpan.getInstance(); + } + + final @Nullable TracesSamplingDecision samplingDecision = + parentSpan == null ? null : parentSpan.getSamplingDecision(); + final @Nullable IOtelSpanWrapper span = + createSpanInternal(scopes, spanOptions, parentSpan, samplingDecision, spanContext); + if (span == null) { + return NoOpSpan.getInstance(); + } + return span; + } + + private @Nullable IOtelSpanWrapper createSpanInternal( + final @NotNull IScopes scopes, + final @NotNull SpanOptions spanOptions, + final @Nullable ISpan parentSpan, + final @Nullable TracesSamplingDecision samplingDecision, + final @NotNull SpanContext spanContext) { + final @NotNull String name = spanContext.getOperation(); + final @NotNull SpanBuilder spanBuilder = getTracer().spanBuilder(name); + if (parentSpan == null) { + final @NotNull SentryId traceId = spanContext.getTraceId(); + final @Nullable SpanId parentSpanId = spanContext.getParentSpanId(); + if (parentSpanId == null) { + final @NotNull io.opentelemetry.api.trace.SpanContext otelSpanContext = + io.opentelemetry.api.trace.SpanContext.create( + traceId.toString(), + io.opentelemetry.api.trace.SpanId.getInvalid(), + TraceFlags.getSampled(), + TraceState.getDefault()); + final @NotNull Span wrappedSpan = Span.wrap(otelSpanContext); + spanBuilder.setParent(wrappedSpan.storeInContext(Context.current())); + } else { + final @NotNull io.opentelemetry.api.trace.SpanContext otelSpanContext = + io.opentelemetry.api.trace.SpanContext.createFromRemoteParent( + traceId.toString(), + parentSpanId.toString(), + TraceFlags.getSampled(), + TraceState.getDefault()); + final @NotNull Span wrappedSpan = Span.wrap(otelSpanContext); + spanBuilder.setParent(wrappedSpan.storeInContext(Context.current())); + } + } else { + if (parentSpan instanceof IOtelSpanWrapper) { + IOtelSpanWrapper parentSpanWrapper = (IOtelSpanWrapper) parentSpan; + spanBuilder.setParent(parentSpanWrapper.storeInContext(Context.current())); + } + } + + // note: won't go through propagators + final @Nullable Baggage baggage = spanContext.getBaggage(); + if (baggage != null) { + spanBuilder.setAttribute(InternalSemanticAttributes.BAGGAGE_MUTABLE, baggage.isMutable()); + spanBuilder.setAttribute(InternalSemanticAttributes.BAGGAGE, baggage.toHeaderString(null)); + } + + final @Nullable SentryDate startTimestampFromOptions = spanOptions.getStartTimestamp(); + final @NotNull SentryDate startTimestamp = + startTimestampFromOptions == null + ? scopes.getOptions().getDateProvider().now() + : startTimestampFromOptions; + spanBuilder.setStartTimestamp(startTimestamp.nanoTimestamp(), TimeUnit.NANOSECONDS); + + if (samplingDecision != null) { + spanBuilder.setAttribute(InternalSemanticAttributes.SAMPLED, samplingDecision.getSampled()); + spanBuilder.setAttribute( + InternalSemanticAttributes.SAMPLE_RATE, samplingDecision.getSampleRate()); + spanBuilder.setAttribute( + InternalSemanticAttributes.PROFILE_SAMPLED, samplingDecision.getProfileSampled()); + spanBuilder.setAttribute( + InternalSemanticAttributes.PROFILE_SAMPLE_RATE, samplingDecision.getProfileSampleRate()); + } + + final @NotNull Span otelSpan = spanBuilder.startSpan(); + + final @Nullable IOtelSpanWrapper sentrySpan = storage.getSentrySpan(otelSpan.getSpanContext()); + if (sentrySpan != null) { + final @Nullable String description = spanContext.getDescription(); + if (description != null) { + sentrySpan.setDescription(description); + } + if (spanContext instanceof TransactionContext) { + final @NotNull TransactionContext transactionContext = (TransactionContext) spanContext; + sentrySpan.setTransactionName( + transactionContext.getName(), transactionContext.getTransactionNameSource()); + } + sentrySpan.getSpanContext().setOrigin(spanOptions.getOrigin()); + } + + if (sentrySpan == null) { + return null; + } else { + return new OtelStrongRefSpanWrapper(otelSpan, sentrySpan); + } + } + + private @NotNull Tracer getTracer() { + return getTracerProvider().get("sentry-opentelemetry", BuildConfig.VERSION_NAME); + } + + private @NotNull TracerProvider getTracerProvider() { + if (openTelemetry != null) { + return openTelemetry.getTracerProvider(); + } + return GlobalOpenTelemetry.getTracerProvider(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStorageToken.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStorageToken.java new file mode 100644 index 00000000000..f9c7ccafadc --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStorageToken.java @@ -0,0 +1,21 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.Scope; +import io.sentry.ISentryLifecycleToken; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +final class OtelStorageToken implements ISentryLifecycleToken { + + private final @NotNull Scope otelScope; + + OtelStorageToken(final @NotNull Scope otelScope) { + this.otelScope = otelScope; + } + + @Override + public void close() { + otelScope.close(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java new file mode 100644 index 00000000000..f2ea37b3350 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java @@ -0,0 +1,306 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.sentry.BaggageHeader; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ISpan; +import io.sentry.Instrumenter; +import io.sentry.MeasurementUnit; +import io.sentry.SentryDate; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanContext; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TraceContext; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.MeasurementValue; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This holds a strong reference to the OpenTelemetry span, preventing it from being garbage + * collected. + * + *

    IMPORTANT: Only use this carefully. Please read below. + * + *

    This class should only be used in cases where Sentry SDK is used to create an OpenTelemetry + * span under the hood that no one holds a reference to otherwise. + * + *

    e.g. ITransaction transaction = Sentry.startTransaction(...) Sentry creates an OTel span under + * the hood, but no one would reference it unless this class is used and returned to the user. By + * doing this, we tie the OTel span to the returned Sentry span/transaction which the user can hold + * on to. + */ +@ApiStatus.Internal +public final class OtelStrongRefSpanWrapper implements IOtelSpanWrapper { + + @SuppressWarnings("UnusedVariable") + private final @NotNull Span otelSpan; + + private final @NotNull IOtelSpanWrapper delegate; + + public OtelStrongRefSpanWrapper(@NotNull Span otelSpan, @NotNull IOtelSpanWrapper delegate) { + this.otelSpan = otelSpan; + this.delegate = delegate; + } + + @Override + public void setTransactionName(@NotNull String name) { + delegate.setTransactionName(name); + } + + @Override + public void setTransactionName(@NotNull String name, @NotNull TransactionNameSource nameSource) { + delegate.setTransactionName(name, nameSource); + } + + @Override + public @Nullable TransactionNameSource getTransactionNameSource() { + return delegate.getTransactionNameSource(); + } + + @Override + public @Nullable String getTransactionName() { + return delegate.getTransactionName(); + } + + @Override + public @NotNull SentryId getTraceId() { + return delegate.getTraceId(); + } + + @Override + public @NotNull Map getData() { + return delegate.getData(); + } + + @Override + public @NotNull Map getMeasurements() { + return delegate.getMeasurements(); + } + + @Override + public @Nullable Boolean isProfileSampled() { + return delegate.isProfileSampled(); + } + + @Override + public @NotNull IScopes getScopes() { + return delegate.getScopes(); + } + + @Override + public @NotNull Map getTags() { + return delegate.getTags(); + } + + @Override + public @NotNull Context storeInContext(Context context) { + return delegate.storeInContext(context); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation) { + return delegate.startChild(operation); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return delegate.startChild(operation, description, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull SpanContext spanContext, @NotNull SpanOptions spanOptions) { + return delegate.startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter) { + return delegate.startChild(operation, description, timestamp, instrumenter); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return delegate.startChild(operation, description, timestamp, instrumenter, spanOptions); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation, @Nullable String description) { + return delegate.startChild(operation, description); + } + + @Override + public @NotNull SentryTraceHeader toSentryTrace() { + return delegate.toSentryTrace(); + } + + @Override + public @Nullable TraceContext traceContext() { + return delegate.traceContext(); + } + + @Override + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + return delegate.toBaggageHeader(thirdPartyBaggageHeaders); + } + + @Override + public void finish() { + delegate.finish(); + } + + @Override + public void finish(@Nullable SpanStatus status) { + delegate.finish(status); + } + + @Override + public void finish(@Nullable SpanStatus status, @Nullable SentryDate timestamp) { + delegate.finish(status, timestamp); + } + + @Override + public void setOperation(@NotNull String operation) { + delegate.setOperation(operation); + } + + @Override + public @NotNull String getOperation() { + return delegate.getOperation(); + } + + @Override + public void setDescription(@Nullable String description) { + delegate.setDescription(description); + } + + @Override + public @Nullable String getDescription() { + return delegate.getDescription(); + } + + @Override + public void setStatus(@Nullable SpanStatus status) { + delegate.setStatus(status); + } + + @Override + public @Nullable SpanStatus getStatus() { + return delegate.getStatus(); + } + + @Override + public void setThrowable(@Nullable Throwable throwable) { + delegate.setThrowable(throwable); + } + + @Override + public @Nullable Throwable getThrowable() { + return delegate.getThrowable(); + } + + @Override + public @NotNull SpanContext getSpanContext() { + return delegate.getSpanContext(); + } + + @Override + public void setTag(@NotNull String key, @NotNull String value) { + delegate.setTag(key, value); + } + + @Override + public @Nullable String getTag(@NotNull String key) { + return delegate.getTag(key); + } + + @Override + public boolean isFinished() { + return delegate.isFinished(); + } + + @Override + public void setData(@NotNull String key, @NotNull Object value) { + delegate.setData(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return delegate.getData(key); + } + + @Override + public void setMeasurement(@NotNull String name, @NotNull Number value) { + delegate.setMeasurement(name, value); + } + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) { + delegate.setMeasurement(name, value, unit); + } + + @Override + public boolean updateEndDate(@NotNull SentryDate date) { + return delegate.updateEndDate(date); + } + + @Override + public @NotNull SentryDate getStartDate() { + return delegate.getStartDate(); + } + + @Override + public @Nullable SentryDate getFinishDate() { + return delegate.getFinishDate(); + } + + @Override + public boolean isNoOp() { + return delegate.isNoOp(); + } + + @Override + public void setContext(@NotNull String key, @NotNull Object context) { + delegate.setContext(key, context); + } + + @Override + public @NotNull Contexts getContexts() { + return delegate.getContexts(); + } + + @Override + public @Nullable Boolean isSampled() { + return delegate.isSampled(); + } + + @Override + public @Nullable TracesSamplingDecision getSamplingDecision() { + return delegate.getSamplingDecision(); + } + + @Override + public @NotNull ISentryLifecycleToken makeCurrent() { + return delegate.makeCurrent(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java new file mode 100644 index 00000000000..18e5d1b6db1 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelTransactionSpanForwarder.java @@ -0,0 +1,312 @@ +package io.sentry.opentelemetry; + +import static io.sentry.TransactionContext.DEFAULT_TRANSACTION_NAME; + +import io.sentry.BaggageHeader; +import io.sentry.Hint; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.Instrumenter; +import io.sentry.MeasurementUnit; +import io.sentry.SentryDate; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanContext; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TraceContext; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import io.sentry.util.Objects; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelTransactionSpanForwarder implements ITransaction { + + private final @NotNull IOtelSpanWrapper rootSpan; + + public OtelTransactionSpanForwarder(final @NotNull IOtelSpanWrapper rootSpan) { + this.rootSpan = Objects.requireNonNull(rootSpan, "root span is required"); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation) { + return rootSpan.startChild(operation); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return rootSpan.startChild(operation, description, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull SpanContext spanContext, @NotNull SpanOptions spanOptions) { + return rootSpan.startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter) { + return rootSpan.startChild(operation, description, timestamp, instrumenter); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return rootSpan.startChild(operation, description, timestamp, instrumenter, spanOptions); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation, @Nullable String description) { + return rootSpan.startChild(operation, description); + } + + @Override + public @NotNull SentryTraceHeader toSentryTrace() { + return rootSpan.toSentryTrace(); + } + + @Override + public @Nullable TraceContext traceContext() { + return rootSpan.traceContext(); + } + + @Override + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + return rootSpan.toBaggageHeader(thirdPartyBaggageHeaders); + } + + @Override + public void finish() { + rootSpan.finish(); + } + + @Override + public void finish(@Nullable SpanStatus status) { + rootSpan.finish(status); + } + + @Override + public void finish(@Nullable SpanStatus status, @Nullable SentryDate timestamp) { + rootSpan.finish(status, timestamp); + } + + @Override + public void setOperation(@NotNull String operation) { + rootSpan.startChild(operation); + } + + @Override + public @NotNull String getOperation() { + return rootSpan.getOperation(); + } + + @Override + public void setDescription(@Nullable String description) { + rootSpan.setDescription(description); + } + + @Override + public @Nullable String getDescription() { + return rootSpan.getDescription(); + } + + @Override + public void setStatus(@Nullable SpanStatus status) { + rootSpan.setStatus(status); + } + + @Override + public @Nullable SpanStatus getStatus() { + return rootSpan.getStatus(); + } + + @Override + public void setThrowable(@Nullable Throwable throwable) { + rootSpan.setThrowable(throwable); + } + + @Override + public @Nullable Throwable getThrowable() { + return rootSpan.getThrowable(); + } + + @Override + public @NotNull SpanContext getSpanContext() { + return rootSpan.getSpanContext(); + } + + @Override + public void setTag(@NotNull String key, @NotNull String value) { + rootSpan.setTag(key, value); + } + + @Override + public @Nullable String getTag(@NotNull String key) { + return rootSpan.getTag(key); + } + + @Override + public boolean isFinished() { + return rootSpan.isFinished(); + } + + @Override + public void setData(@NotNull String key, @NotNull Object value) { + rootSpan.setData(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return rootSpan.getData(key); + } + + @Override + public void setMeasurement(@NotNull String name, @NotNull Number value) { + rootSpan.setMeasurement(name, value); + } + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) { + rootSpan.setMeasurement(name, value, unit); + } + + @Override + public boolean updateEndDate(@NotNull SentryDate date) { + return rootSpan.updateEndDate(date); + } + + @Override + public @NotNull SentryDate getStartDate() { + return rootSpan.getStartDate(); + } + + @Override + public @Nullable SentryDate getFinishDate() { + return rootSpan.getFinishDate(); + } + + @Override + public boolean isNoOp() { + return rootSpan.isNoOp(); + } + + @Override + public @NotNull TransactionNameSource getTransactionNameSource() { + final @Nullable TransactionNameSource nameSource = rootSpan.getTransactionNameSource(); + if (nameSource == null) { + return TransactionNameSource.CUSTOM; + } + return nameSource; + } + + @Override + public @NotNull List getSpans() { + return new ArrayList<>(); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @Nullable SentryDate timestamp) { + return rootSpan.startChild(operation, description, timestamp, Instrumenter.SENTRY); + } + + @Override + public @Nullable Boolean isSampled() { + return rootSpan.isSampled(); + } + + @Override + public @Nullable Boolean isProfileSampled() { + return rootSpan.isProfileSampled(); + } + + @Override + public @Nullable TracesSamplingDecision getSamplingDecision() { + return rootSpan.getSamplingDecision(); + } + + @Override + public @Nullable ISpan getLatestActiveSpan() { + return rootSpan; + } + + @Override + public @NotNull SentryId getEventId() { + return new SentryId(); + } + + @ApiStatus.Internal + @Override + public @NotNull ISentryLifecycleToken makeCurrent() { + return rootSpan.makeCurrent(); + } + + @Override + public void scheduleFinish() {} + + @Override + public void forceFinish( + @NotNull SpanStatus status, boolean dropIfNoChildren, @Nullable Hint hint) { + rootSpan.finish(status); + } + + @Override + public void finish( + @Nullable SpanStatus status, + @Nullable SentryDate timestamp, + boolean dropIfNoChildren, + @Nullable Hint hint) { + rootSpan.finish(status, timestamp); + } + + @Override + public void setContext(@NotNull String key, @NotNull Object context) { + // thoughts: + // - span would have to save it on global storage too since we can't add complex data to otel + // span + // - with span ingestion there isn't a transaction anymore, so if we still need Contexts it + // should go on the (root) span + rootSpan.setContext(key, context); + } + + @Override + public @NotNull Contexts getContexts() { + return rootSpan.getContexts(); + } + + @Override + public void setName(@NotNull String name) { + rootSpan.setTransactionName(name); + } + + @Override + public void setName(@NotNull String name, @NotNull TransactionNameSource nameSource) { + rootSpan.setTransactionName(name, nameSource); + } + + @Override + public @NotNull String getName() { + final @Nullable String name = rootSpan.getTransactionName(); + if (name == null) { + return DEFAULT_TRANSACTION_NAME; + } + return name; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java new file mode 100644 index 00000000000..4f3efa40c29 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java @@ -0,0 +1,41 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class SentryContextStorage implements ContextStorage { + private final @NotNull ContextStorage contextStorage; + + public SentryContextStorage(final @NotNull ContextStorage contextStorage) { + this.contextStorage = contextStorage; + } + + @Override + public Scope attach(Context toAttach) { + // TODO [POTEL] do we need to fork here as well? + // scenario: Context is propagated from thread A to thread B without changes + // OTEL likely also dosn't fork in that case so we probably also don't have to + // or maybe shouldn't even to better align with OTEL + // but since OTEL Context is immutable it doesn't have the same consequence for OTEL as for us + + // TODO [POTEL] sometimes context has already gone through forking but is still an + // ArrayBaseContext + // most likely due to OTEL bridging between agent and app + + // incoming non sentry wrapped context that already has scopes in it + if (toAttach instanceof SentryContextWrapper) { + return contextStorage.attach(toAttach); + } else { + return contextStorage.attach(SentryContextWrapper.wrap(toAttach)); + } + } + + @Override + public Context current() { + return contextStorage.current(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java new file mode 100644 index 00000000000..0f2aa97c8d5 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorageProvider.java @@ -0,0 +1,11 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.ContextStorageProvider; + +public final class SentryContextStorageProvider implements ContextStorageProvider { + @Override + public ContextStorage get() { + return new SentryContextStorage(new SentryOtelThreadLocalStorage()); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java new file mode 100644 index 00000000000..a0213bafe65 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -0,0 +1,97 @@ +package io.sentry.opentelemetry; + +import static io.sentry.opentelemetry.SentryOtelKeys.SENTRY_SCOPES_KEY; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.sentry.IScopes; +import io.sentry.Sentry; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SentryContextWrapper implements Context { + + private final @NotNull Context delegate; + + private SentryContextWrapper(final @NotNull Context delegate) { + this.delegate = delegate; + } + + @Override + public V get(final @NotNull ContextKey contextKey) { + return delegate.get(contextKey); + } + + @Override + public Context with(final @NotNull ContextKey contextKey, V v) { + final @NotNull Context modifiedContext = delegate.with(contextKey, v); + + if (isOpentelemetrySpan(contextKey)) { + return forkCurrentScope(modifiedContext); + } else { + return modifiedContext; + } + } + + private boolean isOpentelemetrySpan(final @NotNull ContextKey contextKey) { + return "opentelemetry-trace-span-key".equals(contextKey.toString()); + } + + private static @NotNull Context forkCurrentScope(final @NotNull Context context) { + final @Nullable IOtelSpanWrapper sentrySpan = getCurrentSpanFromGlobalStorage(context); + final @Nullable IScopes spanScopes = sentrySpan == null ? null : sentrySpan.getScopes(); + final @NotNull IScopes forkedScopes = forkCurrentScopeInternal(context, spanScopes); + if (sentrySpan != null) { + forkedScopes.setActiveSpan(sentrySpan); + } + return context.with(SENTRY_SCOPES_KEY, forkedScopes); + } + + private static @NotNull IScopes forkCurrentScopeInternal( + final @NotNull Context context, final @Nullable IScopes spanScopes) { + final @Nullable IScopes scopesInContext = context.get(SENTRY_SCOPES_KEY); + + if (scopesInContext != null && spanScopes != null) { + if (scopesInContext.isAncestorOf(spanScopes)) { + return spanScopes.forkedCurrentScope("contextwrapper.spanancestor"); + } + } + + if (scopesInContext != null) { + return scopesInContext.forkedCurrentScope("contextwrapper.scopeincontext"); + } + + if (spanScopes != null) { + return spanScopes.forkedCurrentScope("contextwrapper.spanscope"); + } + + return Sentry.forkedRootScopes("contextwrapper.fallback"); + } + + private static @Nullable IOtelSpanWrapper getCurrentSpanFromGlobalStorage( + final @NotNull Context context) { + @Nullable final Span span = Span.fromContextOrNull(context); + + if (span != null) { + final @Nullable IOtelSpanWrapper sentrySpan = + SentryWeakSpanStorage.getInstance().getSentrySpan(span.getSpanContext()); + return sentrySpan; + } + + return null; + } + + public static @NotNull SentryContextWrapper wrap(final @NotNull Context context) { + // we have to fork here because the first time we get to wrap a context it may already have a + // span and a scope + return new SentryContextWrapper(forkCurrentScope(context)); + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java similarity index 79% rename from sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java rename to sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java index 51ead00c6f3..54889d1e73d 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java @@ -2,6 +2,7 @@ import io.opentelemetry.context.ContextKey; import io.sentry.Baggage; +import io.sentry.IScopes; import io.sentry.SentryTraceHeader; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -13,4 +14,6 @@ public final class SentryOtelKeys { ContextKey.named("sentry.trace"); public static final @NotNull ContextKey SENTRY_BAGGAGE_KEY = ContextKey.named("sentry.baggage"); + public static final @NotNull ContextKey SENTRY_SCOPES_KEY = + ContextKey.named("sentry.scopes"); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelThreadLocalStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelThreadLocalStorage.java new file mode 100644 index 00000000000..e44afc5a2dc --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelThreadLocalStorage.java @@ -0,0 +1,85 @@ +/* + * Adapted from https://github.com/open-telemetry/opentelemetry-java/blob/0aacc55d1e3f5cc6dbb4f8fa26bcb657b01a7bc9/context/src/main/java/io/opentelemetry/context/ThreadLocalContextStorage.java + * + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.sentry.opentelemetry; + +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * Workaround to make OpenTelemetry context storage work for Sentry since Sentry sometimes forks + * Context without cleaning up. We are not yet sure if this is something we can easliy fix, since + * Sentry static API makes heavy use of getCurrentScopes and there is no easy way of knowing when to + * restore previous Context. + */ +@ApiStatus.Experimental +@ApiStatus.Internal +public final class SentryOtelThreadLocalStorage implements ContextStorage { + private static final Logger logger = + Logger.getLogger(SentryOtelThreadLocalStorage.class.getName()); + + private static final ThreadLocal THREAD_LOCAL_STORAGE = new ThreadLocal<>(); + + @Override + public Scope attach(Context toAttach) { + if (toAttach == null) { + // Null context not allowed so ignore it. + return NoopScope.INSTANCE; + } + + Context beforeAttach = current(); + if (toAttach == beforeAttach) { + return NoopScope.INSTANCE; + } + + THREAD_LOCAL_STORAGE.set(toAttach); + + return new SentryScopeImpl(beforeAttach); + } + + private static class SentryScopeImpl implements Scope { + @Nullable private final Context beforeAttach; + private boolean closed; + + private SentryScopeImpl(@Nullable Context beforeAttach) { + this.beforeAttach = beforeAttach; + } + + @Override + public void close() { + // if (!closed && current() == toAttach) { + // Used to make OTel thread local storage compatible with Sentry where cleanup isn't always + // performed correctly + if (!closed) { + closed = true; + THREAD_LOCAL_STORAGE.set(beforeAttach); + } else { + logger.log( + Level.FINE, + " Trying to close scope which does not represent current context. Ignoring the call."); + } + } + } + + @Override + @Nullable + public Context current() { + return THREAD_LOCAL_STORAGE.get(); + } + + enum NoopScope implements Scope { + INSTANCE; + + @Override + public void close() {} + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java new file mode 100644 index 00000000000..c28d4ed7ffb --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -0,0 +1,47 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.sentry.ISentryLifecycleToken; +import io.sentry.util.AutoClosableReentrantLock; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Weakly references wrappers for OpenTelemetry spans meaning they'll be cleaned up when the + * OpenTelemetry span is garbage collected. + */ +@ApiStatus.Internal +public final class SentryWeakSpanStorage { + private static volatile @Nullable SentryWeakSpanStorage INSTANCE; + private static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); + + public static @NotNull SentryWeakSpanStorage getInstance() { + if (INSTANCE == null) { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { + if (INSTANCE == null) { + INSTANCE = new SentryWeakSpanStorage(); + } + } + } + + return INSTANCE; + } + + // weak keys, spawns a thread to clean up values that have been garbage collected + private final @NotNull WeakConcurrentMap sentrySpans = + new WeakConcurrentMap<>(true); + + private SentryWeakSpanStorage() {} + + public @Nullable IOtelSpanWrapper getSentrySpan(final @NotNull SpanContext spanContext) { + return sentrySpans.get(spanContext); + } + + public void storeSentrySpan( + final @NotNull SpanContext otelSpan, final @NotNull IOtelSpanWrapper sentrySpan) { + this.sentrySpans.put(otelSpan, sentrySpan); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider new file mode 100644 index 00000000000..87a43eea486 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider @@ -0,0 +1 @@ +io.sentry.opentelemetry.SentryContextStorageProvider diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api index 18c73a9b689..fd2099a7303 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -1,8 +1,42 @@ public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor { public fun ()V + public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public final class io/sentry/opentelemetry/OtelInternalSpanDetectionUtil { + public fun ()V + public static fun isSentryRequest (Lio/sentry/IScopes;Lio/opentelemetry/api/trace/SpanKind;Lio/opentelemetry/api/common/Attributes;)Z +} + +public final class io/sentry/opentelemetry/OtelSamplingUtil { + public fun ()V + public static fun extractSamplingDecision (Lio/opentelemetry/api/common/Attributes;)Lio/sentry/TracesSamplingDecision; +} + +public final class io/sentry/opentelemetry/OtelSentryPropagator : io/opentelemetry/context/propagation/TextMapPropagator { + public fun ()V + public fun extract (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapGetter;)Lio/opentelemetry/context/Context; + public fun fields ()Ljava/util/Collection; + public fun inject (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapSetter;)V +} + +public final class io/sentry/opentelemetry/OtelSentrySpanProcessor : io/opentelemetry/sdk/trace/SpanProcessor { + public fun ()V + public fun isEndRequired ()Z + public fun isStartRequired ()Z + public fun onEnd (Lio/opentelemetry/sdk/trace/ReadableSpan;)V + public fun onStart (Lio/opentelemetry/context/Context;Lio/opentelemetry/sdk/trace/ReadWriteSpan;)V +} + +public final class io/sentry/opentelemetry/OtelSpanContext : io/sentry/SpanContext { + public fun (Lio/opentelemetry/sdk/trace/ReadWriteSpan;Lio/sentry/TracesSamplingDecision;Lio/sentry/opentelemetry/IOtelSpanWrapper;Lio/sentry/SpanId;Lio/sentry/Baggage;)V + public fun getOperation ()Ljava/lang/String; + public fun getStatus ()Lio/sentry/SpanStatus; + public fun setOperation (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/SpanStatus;)V +} + public final class io/sentry/opentelemetry/OtelSpanInfo { public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun getDescription ()Ljava/lang/String; @@ -10,10 +44,56 @@ public final class io/sentry/opentelemetry/OtelSpanInfo { public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; } -public final class io/sentry/opentelemetry/SentryOtelKeys { - public static final field SENTRY_BAGGAGE_KEY Lio/opentelemetry/context/ContextKey; - public static final field SENTRY_TRACE_KEY Lio/opentelemetry/context/ContextKey; - public fun ()V +public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { + public fun (Lio/opentelemetry/sdk/trace/ReadWriteSpan;Lio/sentry/IScopes;Lio/sentry/SentryDate;Lio/sentry/TracesSamplingDecision;Lio/sentry/opentelemetry/IOtelSpanWrapper;Lio/sentry/SpanId;Lio/sentry/Baggage;)V + public fun finish ()V + public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V + public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getData ()Ljava/util/Map; + public fun getData (Ljava/lang/String;)Ljava/lang/Object; + public fun getDescription ()Ljava/lang/String; + public fun getFinishDate ()Lio/sentry/SentryDate; + public fun getMeasurements ()Ljava/util/Map; + public fun getOperation ()Ljava/lang/String; + public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; + public fun getScopes ()Lio/sentry/IScopes; + public fun getSpanContext ()Lio/sentry/SpanContext; + public fun getStartDate ()Lio/sentry/SentryDate; + public fun getStatus ()Lio/sentry/SpanStatus; + public fun getTag (Ljava/lang/String;)Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public fun getThrowable ()Ljava/lang/Throwable; + public fun getTraceId ()Lio/sentry/protocol/SentryId; + public fun getTransactionName ()Ljava/lang/String; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public fun isFinished ()Z + public fun isNoOp ()Z + public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isSampled ()Ljava/lang/Boolean; + public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V + public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setOperation (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/SpanStatus;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setThrowable (Ljava/lang/Throwable;)V + public fun setTransactionName (Ljava/lang/String;)V + public fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; + public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; + public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; + public fun traceContext ()Lio/sentry/TraceContext; + public fun updateEndDate (Lio/sentry/SentryDate;)Z } public final class io/sentry/opentelemetry/SentryPropagator : io/opentelemetry/context/propagation/TextMapPropagator { @@ -23,6 +103,29 @@ public final class io/sentry/opentelemetry/SentryPropagator : io/opentelemetry/c public fun inject (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapSetter;)V } +public final class io/sentry/opentelemetry/SentrySampler : io/opentelemetry/sdk/trace/samplers/Sampler { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun getDescription ()Ljava/lang/String; + public fun shouldSample (Lio/opentelemetry/context/Context;Ljava/lang/String;Ljava/lang/String;Lio/opentelemetry/api/trace/SpanKind;Lio/opentelemetry/api/common/Attributes;Ljava/util/List;)Lio/opentelemetry/sdk/trace/samplers/SamplingResult; +} + +public final class io/sentry/opentelemetry/SentrySamplingResult : io/opentelemetry/sdk/trace/samplers/SamplingResult { + public fun (Lio/sentry/TracesSamplingDecision;)V + public fun getAttributes ()Lio/opentelemetry/api/common/Attributes; + public fun getDecision ()Lio/opentelemetry/sdk/trace/samplers/SamplingDecision; + public fun getSentryDecision ()Lio/sentry/TracesSamplingDecision; +} + +public final class io/sentry/opentelemetry/SentrySpanExporter : io/opentelemetry/sdk/trace/export/SpanExporter { + public static final field TRACE_ORIGIN Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun export (Ljava/util/Collection;)Lio/opentelemetry/sdk/common/CompletableResultCode; + public fun flush ()Lio/opentelemetry/sdk/common/CompletableResultCode; + public fun shutdown ()Lio/opentelemetry/sdk/common/CompletableResultCode; +} + public final class io/sentry/opentelemetry/SentrySpanProcessor : io/opentelemetry/sdk/trace/SpanProcessor { public fun ()V public fun isEndRequired ()Z @@ -33,7 +136,19 @@ public final class io/sentry/opentelemetry/SentrySpanProcessor : io/opentelemetr public final class io/sentry/opentelemetry/SpanDescriptionExtractor { public fun ()V - public fun extractSpanDescription (Lio/opentelemetry/sdk/trace/ReadableSpan;)Lio/sentry/opentelemetry/OtelSpanInfo; + public fun extractSpanInfo (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/opentelemetry/IOtelSpanWrapper;)Lio/sentry/opentelemetry/OtelSpanInfo; +} + +public final class io/sentry/opentelemetry/SpanNode { + public fun (Ljava/lang/String;)V + public fun addChild (Lio/sentry/opentelemetry/SpanNode;)V + public fun addChildren (Ljava/util/List;)V + public fun getChildren ()Ljava/util/List; + public fun getId ()Ljava/lang/String; + public fun getParentNode ()Lio/sentry/opentelemetry/SpanNode; + public fun getSpan ()Lio/opentelemetry/sdk/trace/data/SpanData; + public fun setParentNode (Lio/sentry/opentelemetry/SpanNode;)V + public fun setSpan (Lio/opentelemetry/sdk/trace/data/SpanData;)V } public final class io/sentry/opentelemetry/TraceData { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts index 1dad433555e..e46ff2783a6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -20,9 +20,16 @@ tasks.withType().configureEach { dependencies { compileOnly(projects.sentry) + /** + * sentryOpentelemetryBootstrap cannot be an implementation dependency + * because getSentryOpentelemetryCore is loaded into the agent classloader + * and sentryOpentelemetryBootstrap should be in the bootstrap classloader. + */ + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) implementation(Config.Libs.OpenTelemetry.otelSdk) compileOnly(Config.Libs.OpenTelemetry.otelSemconv) + compileOnly(Config.Libs.OpenTelemetry.otelSemconvIncubating) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) @@ -31,6 +38,7 @@ dependencies { errorprone(Config.CompileOnly.errorProneNullAway) // tests + testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) @@ -39,6 +47,7 @@ dependencies { testImplementation(Config.Libs.OpenTelemetry.otelSdk) testImplementation(Config.Libs.OpenTelemetry.otelSemconv) + testImplementation(Config.Libs.OpenTelemetry.otelSemconvIncubating) } configure { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor.java index 1e373ece9c0..cf1e530cd9e 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor.java @@ -5,36 +5,42 @@ import io.opentelemetry.api.trace.TraceId; import io.sentry.EventProcessor; import io.sentry.Hint; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.Instrumenter; +import io.sentry.ScopesAdapter; import io.sentry.SentryEvent; import io.sentry.SentryLevel; -import io.sentry.SentrySpanStorage; import io.sentry.SpanContext; import io.sentry.protocol.SentryId; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; +/** + * @deprecated this is no longer needed for the latest version of our OpenTelemetry integration. + */ +@Deprecated public final class OpenTelemetryLinkErrorEventProcessor implements EventProcessor { - private final @NotNull IHub hub; - private final @NotNull SentrySpanStorage spanStorage = SentrySpanStorage.getInstance(); + private final @NotNull IScopes scopes; + + @SuppressWarnings("deprecation") + private final @NotNull io.sentry.SentrySpanStorage spanStorage = + io.sentry.SentrySpanStorage.getInstance(); public OpenTelemetryLinkErrorEventProcessor() { - this(HubAdapter.getInstance()); + this(ScopesAdapter.getInstance()); } @TestOnly - OpenTelemetryLinkErrorEventProcessor(final @NotNull IHub hub) { - this.hub = hub; + OpenTelemetryLinkErrorEventProcessor(final @NotNull IScopes scopes) { + this.scopes = scopes; } @Override public @Nullable SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { - final @NotNull Instrumenter instrumenter = hub.getOptions().getInstrumenter(); + final @NotNull Instrumenter instrumenter = scopes.getOptions().getInstrumenter(); if (Instrumenter.OTEL.equals(instrumenter)) { @NotNull final Span otelSpan = Span.current(); @NotNull final String traceId = otelSpan.getSpanContext().getTraceId(); @@ -55,7 +61,8 @@ public OpenTelemetryLinkErrorEventProcessor() { null); event.getContexts().setTrace(spanContext); - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -64,7 +71,8 @@ public OpenTelemetryLinkErrorEventProcessor() { spanId, traceId); } else { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -74,7 +82,8 @@ public OpenTelemetryLinkErrorEventProcessor() { traceId); } } else { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -84,7 +93,8 @@ public OpenTelemetryLinkErrorEventProcessor() { spanId); } } else { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -95,4 +105,9 @@ public OpenTelemetryLinkErrorEventProcessor() { return event; } + + @Override + public @Nullable Long getOrder() { + return 6000L; + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java new file mode 100644 index 00000000000..e4bd5a7e7c8 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java @@ -0,0 +1,65 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.semconv.UrlAttributes; +import io.sentry.DsnUtil; +import io.sentry.IScopes; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelInternalSpanDetectionUtil { + + private static final @NotNull List spanKindsConsideredForSentryRequests = + Arrays.asList(SpanKind.CLIENT, SpanKind.INTERNAL); + + @SuppressWarnings("deprecation") + public static boolean isSentryRequest( + final @NotNull IScopes scopes, + final @NotNull SpanKind spanKind, + final @NotNull Attributes attributes) { + if (!spanKindsConsideredForSentryRequests.contains(spanKind)) { + return false; + } + + final @Nullable String httpUrl = + attributes.get(io.opentelemetry.semconv.SemanticAttributes.HTTP_URL); + if (DsnUtil.urlContainsDsnHost(scopes.getOptions(), httpUrl)) { + return true; + } + + final @Nullable String fullUrl = attributes.get(UrlAttributes.URL_FULL); + if (DsnUtil.urlContainsDsnHost(scopes.getOptions(), fullUrl)) { + return true; + } + + if (scopes.getOptions().isEnableSpotlight()) { + final @Nullable String optionsSpotlightUrl = scopes.getOptions().getSpotlightConnectionUrl(); + final @NotNull String spotlightUrl = + optionsSpotlightUrl != null ? optionsSpotlightUrl : "http://localhost:8969/stream"; + + if (containsSpotlightUrl(fullUrl, spotlightUrl)) { + return true; + } + if (containsSpotlightUrl(httpUrl, spotlightUrl)) { + return true; + } + } + + return false; + } + + private static boolean containsSpotlightUrl( + final @Nullable String requestUrl, final @NotNull String spotlightUrl) { + if (requestUrl == null) { + return false; + } + + return requestUrl.toLowerCase(Locale.ROOT).contains(spotlightUrl.toLowerCase(Locale.ROOT)); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java new file mode 100644 index 00000000000..01f414f9022 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSamplingUtil.java @@ -0,0 +1,28 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.sentry.TracesSamplingDecision; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelSamplingUtil { + + public static @Nullable TracesSamplingDecision extractSamplingDecision( + final @NotNull Attributes attributes) { + final @Nullable Boolean sampled = attributes.get(InternalSemanticAttributes.SAMPLED); + if (sampled != null) { + final @Nullable Double sampleRate = attributes.get(InternalSemanticAttributes.SAMPLE_RATE); + final @Nullable Boolean profileSampled = + attributes.get(InternalSemanticAttributes.PROFILE_SAMPLED); + final @Nullable Double profileSampleRate = + attributes.get(InternalSemanticAttributes.PROFILE_SAMPLE_RATE); + + return new TracesSamplingDecision( + sampled, sampleRate, profileSampled == null ? false : profileSampled, profileSampleRate); + } else { + return null; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java new file mode 100644 index 00000000000..c5802df2454 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java @@ -0,0 +1,145 @@ +package io.sentry.opentelemetry; + +import static io.sentry.opentelemetry.SentryOtelKeys.SENTRY_SCOPES_KEY; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.sentry.Baggage; +import io.sentry.BaggageHeader; +import io.sentry.IScopes; +import io.sentry.PropagationContext; +import io.sentry.ScopesAdapter; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.SentryTraceHeader; +import io.sentry.exception.InvalidSentryTraceHeaderException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class OtelSentryPropagator implements TextMapPropagator { + + private static final @NotNull List FIELDS = + Arrays.asList(SentryTraceHeader.SENTRY_TRACE_HEADER, BaggageHeader.BAGGAGE_HEADER); + private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); + private final @NotNull IScopes scopes; + + public OtelSentryPropagator() { + this(ScopesAdapter.getInstance()); + } + + OtelSentryPropagator(final @NotNull IScopes scopes) { + this.scopes = scopes; + } + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(final Context context, final C carrier, final TextMapSetter setter) { + final @NotNull Span otelSpan = Span.fromContext(context); + final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); + if (!otelSpanContext.isValid()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Not injecting Sentry tracing information for invalid OpenTelemetry span."); + return; + } + + final @Nullable IOtelSpanWrapper sentrySpan = spanStorage.getSentrySpan(otelSpanContext); + if (sentrySpan == null || sentrySpan.isNoOp()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Not injecting Sentry tracing information for span %s as no Sentry span has been found or it is a NoOp (trace %s). This might simply mean this is a request to Sentry.", + otelSpanContext.getSpanId(), + otelSpanContext.getTraceId()); + return; + } + + final @NotNull SentryTraceHeader sentryTraceHeader = sentrySpan.toSentryTrace(); + setter.set(carrier, sentryTraceHeader.getName(), sentryTraceHeader.getValue()); + final @Nullable BaggageHeader baggageHeader = + sentrySpan.toBaggageHeader(Collections.emptyList()); + if (baggageHeader != null) { + setter.set(carrier, baggageHeader.getName(), baggageHeader.getValue()); + } + } + + @Override + public Context extract( + final Context context, final C carrier, final TextMapGetter getter) { + final @Nullable IScopes scopesFromParentContext = context.get(SENTRY_SCOPES_KEY); + final @NotNull IScopes scopesToUse = + scopesFromParentContext != null + ? scopesFromParentContext.forkedScopes("propagator") + : Sentry.forkedRootScopes("propagator"); + + final @Nullable String sentryTraceString = + getter.get(carrier, SentryTraceHeader.SENTRY_TRACE_HEADER); + if (sentryTraceString == null) { + return context.with(SENTRY_SCOPES_KEY, scopesToUse); + } + + try { + SentryTraceHeader sentryTraceHeader = new SentryTraceHeader(sentryTraceString); + + final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER); + final Baggage baggage = Baggage.fromHeader(baggageString); + final @NotNull TraceState traceState = TraceState.getDefault(); + + SpanContext otelSpanContext = + SpanContext.createFromRemoteParent( + sentryTraceHeader.getTraceId().toString(), + sentryTraceHeader.getSpanId().toString(), + TraceFlags.getSampled(), + traceState); + + Span wrappedSpan = Span.wrap(otelSpanContext); + + final @NotNull Context modifiedContext = + context + .with(wrappedSpan) + .with(SENTRY_SCOPES_KEY, scopesToUse) + .with(SentryOtelKeys.SENTRY_TRACE_KEY, sentryTraceHeader) + .with(SentryOtelKeys.SENTRY_BAGGAGE_KEY, baggage); + + scopes + .getOptions() + .getLogger() + .log(SentryLevel.DEBUG, "Continuing Sentry trace %s", sentryTraceHeader.getTraceId()); + + final @NotNull PropagationContext propagationContext = + PropagationContext.fromHeaders( + scopes.getOptions().getLogger(), sentryTraceString, baggageString); + scopesToUse.getIsolationScope().setPropagationContext(propagationContext); + + return modifiedContext; + } catch (InvalidSentryTraceHeaderException e) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.ERROR, + "Unable to extract Sentry tracing information from invalid header.", + e); + return context; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java new file mode 100644 index 00000000000..521bc9020c0 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -0,0 +1,184 @@ +package io.sentry.opentelemetry; + +import static io.sentry.opentelemetry.InternalSemanticAttributes.IS_REMOTE_PARENT; +import static io.sentry.opentelemetry.SentryOtelKeys.SENTRY_SCOPES_KEY; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.sentry.Baggage; +import io.sentry.IScopes; +import io.sentry.PropagationContext; +import io.sentry.ScopesAdapter; +import io.sentry.Sentry; +import io.sentry.SentryDate; +import io.sentry.SentryLevel; +import io.sentry.SentryLongDate; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanId; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class OtelSentrySpanProcessor implements SpanProcessor { + private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); + private final @NotNull IScopes scopes; + + public OtelSentrySpanProcessor() { + this(ScopesAdapter.getInstance()); + } + + OtelSentrySpanProcessor(final @NotNull IScopes scopes) { + this.scopes = scopes; + } + + @Override + public void onStart(final @NotNull Context parentContext, final @NotNull ReadWriteSpan otelSpan) { + if (!ensurePrerequisites(otelSpan)) { + return; + } + + final @Nullable IScopes scopesFromContext = parentContext.get(SENTRY_SCOPES_KEY); + final @NotNull IScopes scopes = + scopesFromContext != null + ? scopesFromContext.forkedCurrentScope("spanprocessor") + : Sentry.forkedRootScopes("spanprocessor"); + + final @Nullable IOtelSpanWrapper sentryParentSpan = + spanStorage.getSentrySpan(otelSpan.getParentSpanContext()); + @Nullable + TracesSamplingDecision samplingDecision = + OtelSamplingUtil.extractSamplingDecision(otelSpan.toSpanData().getAttributes()); + @Nullable Baggage baggage = null; + @Nullable SpanId sentryParentSpanId = null; + otelSpan.setAttribute(IS_REMOTE_PARENT, otelSpan.getParentSpanContext().isRemote()); + if (sentryParentSpan == null) { + final @NotNull String traceId = otelSpan.getSpanContext().getTraceId(); + final @NotNull String spanId = otelSpan.getSpanContext().getSpanId(); + final @NotNull SpanId sentrySpanId = new SpanId(spanId); + final @NotNull String parentSpanId = otelSpan.getParentSpanContext().getSpanId(); + sentryParentSpanId = + io.opentelemetry.api.trace.SpanId.isValid(parentSpanId) ? new SpanId(parentSpanId) : null; + + @Nullable + SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); + @Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); + if (sentryTraceHeader != null) { + baggage = baggageFromContext; + } + + final @Nullable Boolean baggageMutable = + otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE_MUTABLE); + final @Nullable String baggageString = + otelSpan.getAttribute(InternalSemanticAttributes.BAGGAGE); + if (baggageString != null) { + baggage = Baggage.fromHeader(baggageString); + if (baggageMutable == true) { + baggage.freeze(); + } + } + + final @Nullable Boolean sampled = isSampled(otelSpan, samplingDecision); + + final @NotNull PropagationContext propagationContext = + new PropagationContext( + new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled); + + updatePropagationContext(scopes, propagationContext); + } + + final @NotNull SpanContext spanContext = otelSpan.getSpanContext(); + final @NotNull SentryDate startTimestamp = + new SentryLongDate(otelSpan.toSpanData().getStartEpochNanos()); + final @NotNull IOtelSpanWrapper sentrySpan = + new OtelSpanWrapper( + otelSpan, + scopes, + startTimestamp, + samplingDecision, + sentryParentSpan, + sentryParentSpanId, + baggage); + sentrySpan.getSpanContext().setOrigin(SentrySpanExporter.TRACE_ORIGIN); + spanStorage.storeSentrySpan(spanContext, sentrySpan); + } + + private @Nullable Boolean isSampled( + final @NotNull ReadWriteSpan otelSpan, + final @Nullable TracesSamplingDecision samplingDecision) { + if (samplingDecision != null) { + return samplingDecision.getSampled(); + } + + if (otelSpan.getSpanContext().isSampled()) { + return true; + } + + // tracing without performance + return null; + } + + private static void updatePropagationContext( + IScopes scopes, PropagationContext propagationContext) { + scopes.configureScope( + scope -> { + scope.withPropagationContext( + oldPropagationContext -> { + scope.setPropagationContext(propagationContext); + }); + }); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(final @NotNull ReadableSpan spanBeingEnded) { + final @Nullable IOtelSpanWrapper sentrySpan = + spanStorage.getSentrySpan(spanBeingEnded.getSpanContext()); + if (sentrySpan != null) { + final @NotNull SentryDate finishDate = + new SentryLongDate(spanBeingEnded.toSpanData().getEndEpochNanos()); + sentrySpan.updateEndDate(finishDate); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + private boolean ensurePrerequisites(final @NotNull ReadableSpan otelSpan) { + if (!hasSentryBeenInitialized()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Not forwarding OpenTelemetry span to Sentry as Sentry has not yet been initialized."); + return false; + } + + final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); + if (!otelSpanContext.isValid()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Not forwarding OpenTelemetry span to Sentry as the span is invalid."); + return false; + } + + return true; + } + + private boolean hasSentryBeenInitialized() { + return scopes.isEnabled(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java new file mode 100644 index 00000000000..fd7313cfea2 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java @@ -0,0 +1,116 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.sentry.Baggage; +import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanStatus; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.SentryId; +import java.lang.ref.WeakReference; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OtelSpanContext extends SpanContext { + + /** + * OpenTelemetry span which this wrapper wraps. Needs to be referenced weakly as otherwise we'd + * create a circular reference from {@link io.opentelemetry.sdk.trace.data.SpanData} to {@link + * OtelSpanWrapper} and indirectly back to {@link io.opentelemetry.sdk.trace.data.SpanData} via + * {@link Span}. Also see {@link SentryWeakSpanStorage}. + */ + private final @NotNull WeakReference span; + + public OtelSpanContext( + final @NotNull ReadWriteSpan span, + final @Nullable TracesSamplingDecision samplingDecision, + final @Nullable IOtelSpanWrapper parentSpan, + final @Nullable SpanId parentSpanId, + final @Nullable Baggage baggage) { + super( + new SentryId(span.getSpanContext().getTraceId()), + new SpanId(span.getSpanContext().getSpanId()), + parentSpan == null ? parentSpanId : parentSpan.getSpanContext().getSpanId(), + span.getName(), + null, + samplingDecision != null + ? samplingDecision + : (parentSpan == null ? null : parentSpan.getSamplingDecision()), + null, + null); + this.span = new WeakReference<>(span); + this.baggage = baggage; + } + + @Override + public @Nullable SpanStatus getStatus() { + final @Nullable ReadWriteSpan otelSpan = span.get(); + + if (otelSpan != null) { + final @NotNull StatusData otelStatus = otelSpan.toSpanData().getStatus(); + final @NotNull String otelStatusDescription = otelStatus.getDescription(); + if (otelStatusDescription.isEmpty()) { + return otelStatusCodeFallback(otelStatus); + } + final @Nullable SpanStatus spanStatus = SpanStatus.fromApiNameSafely(otelStatusDescription); + if (spanStatus == null) { + return otelStatusCodeFallback(otelStatus); + } + return spanStatus; + } + + return null; + } + + @Override + public void setStatus(@Nullable SpanStatus status) { + if (status != null) { + final @Nullable ReadWriteSpan otelSpan = span.get(); + if (otelSpan != null) { + final @NotNull StatusCode statusCode = translateStatusCode(status); + otelSpan.setStatus(statusCode, status.apiName()); + } + } + } + + @Override + public @NotNull String getOperation() { + final @Nullable ReadWriteSpan otelSpan = span.get(); + if (otelSpan != null) { + return otelSpan.getName(); + } + return ""; + } + + @Override + public void setOperation(@NotNull String operation) { + final @Nullable ReadWriteSpan otelSpan = span.get(); + if (otelSpan != null) { + otelSpan.updateName(operation); + } + } + + private @Nullable SpanStatus otelStatusCodeFallback(final @NotNull StatusData otelStatus) { + if (otelStatus.getStatusCode() == StatusCode.ERROR) { + return SpanStatus.UNKNOWN_ERROR; + } else if (otelStatus.getStatusCode() == StatusCode.OK) { + return SpanStatus.OK; + } + return null; + } + + private @NotNull StatusCode translateStatusCode(final @Nullable SpanStatus status) { + if (status == null) { + return StatusCode.UNSET; + } else if (status == SpanStatus.OK) { + return StatusCode.OK; + } else { + return StatusCode.ERROR; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java index 6ad39d37939..d6a9aab202d 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java @@ -3,17 +3,18 @@ import io.sentry.protocol.TransactionNameSource; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class OtelSpanInfo { private final @NotNull String op; - private final @NotNull String description; + private final @Nullable String description; private final @NotNull TransactionNameSource transactionNameSource; public OtelSpanInfo( final @NotNull String op, - final @NotNull String description, + final @Nullable String description, final @NotNull TransactionNameSource transactionNameSource) { this.op = op; this.description = description; @@ -24,7 +25,7 @@ public OtelSpanInfo( return op; } - public @NotNull String getDescription() { + public @Nullable String getDescription() { return description; } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java new file mode 100644 index 00000000000..090d317485b --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -0,0 +1,526 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.sentry.Baggage; +import io.sentry.BaggageHeader; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ISpan; +import io.sentry.Instrumenter; +import io.sentry.MeasurementUnit; +import io.sentry.NoOpScopesLifecycleToken; +import io.sentry.NoOpSpan; +import io.sentry.ScopeBindingMode; +import io.sentry.SentryDate; +import io.sentry.SentryLevel; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TraceContext; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.MeasurementValue; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.Objects; +import java.lang.ref.WeakReference; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** NOTE: This wrapper is not used when using OpenTelemetry API, only when using Sentry API. */ +@ApiStatus.Internal +public final class OtelSpanWrapper implements IOtelSpanWrapper { + + private final @NotNull IScopes scopes; + + /** The moment in time when span was started. */ + private @NotNull SentryDate startTimestamp; + + private @Nullable SentryDate finishedTimestamp = null; + + /** + * OpenTelemetry span which this wrapper wraps. Needs to be referenced weakly as otherwise we'd + * create a circular reference from {@link io.opentelemetry.sdk.trace.data.SpanData} to {@link + * OtelSpanWrapper} and indirectly back to {@link io.opentelemetry.sdk.trace.data.SpanData} via + * {@link Span}. Also see {@link SentryWeakSpanStorage}. + */ + private final @NotNull WeakReference span; + + private final @NotNull SpanContext context; + private final @NotNull Contexts contexts = new Contexts(); + private @Nullable String transactionName; + private @Nullable TransactionNameSource transactionNameSource; + private final @Nullable Baggage baggage; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + + private final @NotNull Map data = new ConcurrentHashMap<>(); + private final @NotNull Map measurements = new ConcurrentHashMap<>(); + + /** A throwable thrown during the execution of the span. */ + private @Nullable Throwable throwable; + + private @NotNull Deque tokensToCleanup = new ArrayDeque<>(1); + + public OtelSpanWrapper( + final @NotNull ReadWriteSpan span, + final @NotNull IScopes scopes, + final @NotNull SentryDate startTimestamp, + final @Nullable TracesSamplingDecision samplingDecision, + final @Nullable IOtelSpanWrapper parentSpan, + final @Nullable SpanId parentSpanId, + final @Nullable Baggage baggage) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + this.span = new WeakReference<>(span); + this.startTimestamp = startTimestamp; + + if (parentSpan != null) { + this.baggage = parentSpan.getSpanContext().getBaggage(); + } else if (baggage != null) { + this.baggage = baggage; + } else { + this.baggage = null; + } + + this.context = + new OtelSpanContext(span, samplingDecision, parentSpan, parentSpanId, this.baggage); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation) { + return startChild(operation, (String) null); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + if (isFinished()) { + return NoOpSpan.getInstance(); + } + final @NotNull SpanContext spanContext = + context.copyForChild(operation, getSpanContext().getSpanId(), null); + spanContext.setDescription(description); + + return startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull SpanContext spanContext, @NotNull SpanOptions spanOptions) { + if (isFinished()) { + return NoOpSpan.getInstance(); + } + + final @NotNull ISpan childSpan = + scopes.getOptions().getSpanFactory().createSpan(scopes, spanOptions, spanContext, this); + + if (ScopeBindingMode.ON == spanOptions.getScopeBindingMode()) { + childSpan.makeCurrent(); + } else if (ScopeBindingMode.AUTO == spanOptions.getScopeBindingMode()) { + final @Nullable SpanId parentSpanId = spanContext.getParentSpanId(); + if (parentSpanId != null) { + final @Nullable Span currentOtelSpan = Span.fromContextOrNull(Context.current()); + if (currentOtelSpan != null) { + if (currentOtelSpan + .getSpanContext() + .getSpanId() + .equalsIgnoreCase(parentSpanId.toString())) { + childSpan.makeCurrent(); + } + } + } + } + return childSpan; + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter) { + final @NotNull SpanContext spanContext = + context.copyForChild(operation, getSpanContext().getSpanId(), null); + spanContext.setDescription(description); + spanContext.setInstrumenter(instrumenter); + + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setStartTimestamp(timestamp); + + return startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + if (timestamp != null) { + spanOptions.setStartTimestamp(timestamp); + } + + final @NotNull SpanContext spanContext = + context.copyForChild(operation, getSpanContext().getSpanId(), null); + spanContext.setDescription(description); + spanContext.setInstrumenter(instrumenter); + + return startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation, @Nullable String description) { + final @NotNull SpanContext spanContext = + context.copyForChild(operation, getSpanContext().getSpanId(), null); + spanContext.setDescription(description); + + return startChild(spanContext, new SpanOptions()); + } + + @Override + public @NotNull SentryTraceHeader toSentryTrace() { + return new SentryTraceHeader(getTraceId(), getOtelSpanId(), isSampled()); + } + + private @NotNull SpanId getOtelSpanId() { + return context.getSpanId(); + } + + private @Nullable ReadWriteSpan getSpan() { + return span.get(); + } + + @Override + public @Nullable TraceContext traceContext() { + if (scopes.getOptions().isTraceSampling()) { + if (baggage != null) { + updateBaggageValues(); + return baggage.toTraceContext(); + } + } + return null; + } + + private void updateBaggageValues() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (baggage != null && baggage.isMutable()) { + final AtomicReference replayIdAtomicReference = new AtomicReference<>(); + scopes.configureScope( + scope -> { + replayIdAtomicReference.set(scope.getReplayId()); + }); + baggage.setValuesFromTransaction( + getSpanContext().getTraceId(), + replayIdAtomicReference.get(), + scopes.getOptions(), + this.getSamplingDecision(), + getTransactionName(), + getTransactionNameSource()); + baggage.freeze(); + } + } + } + + @Override + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + if (scopes.getOptions().isTraceSampling()) { + if (baggage != null) { + updateBaggageValues(); + return BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); + } + } + return null; + } + + @Override + public void finish() { + finish(getStatus()); + } + + @Override + public void finish(@Nullable SpanStatus status) { + setStatus(status); + final @Nullable Span otelSpan = getSpan(); + if (otelSpan != null) { + otelSpan.end(); + } + + for (ISentryLifecycleToken token : tokensToCleanup) { + token.close(); + } + } + + @Override + public void finish(@Nullable SpanStatus status, @Nullable SentryDate timestamp) { + setStatus(status); + final @Nullable Span otelSpan = getSpan(); + if (otelSpan != null) { + if (timestamp != null) { + otelSpan.end(timestamp.nanoTimestamp(), TimeUnit.NANOSECONDS); + } else { + otelSpan.end(); + } + } + } + + @Override + public void setOperation(@NotNull String operation) { + this.context.setOperation(operation); + } + + @Override + public @NotNull String getOperation() { + return context.getOperation(); + } + + @Override + public void setDescription(@Nullable String description) { + this.context.setDescription(description); + } + + @Override + public @Nullable String getDescription() { + return this.context.getDescription(); + } + + @Override + public void setStatus(final @Nullable SpanStatus status) { + context.setStatus(status); + } + + @Override + public @Nullable SpanStatus getStatus() { + return context.getStatus(); + } + + @Override + public void setThrowable(@Nullable Throwable throwable) { + this.throwable = throwable; + } + + @Override + public @Nullable Throwable getThrowable() { + return throwable; + } + + @Override + public @NotNull SpanContext getSpanContext() { + return context; + } + + @Override + public void setTag(@NotNull String key, @NotNull String value) { + context.setTag(key, value); + } + + @Override + public @Nullable String getTag(@NotNull String key) { + return context.getTags().get(key); + } + + @Override + @ApiStatus.Internal + public @NotNull Map getTags() { + return context.getTags(); + } + + @Override + public boolean isFinished() { + final @Nullable ReadWriteSpan otelSpan = getSpan(); + if (otelSpan != null) { + return otelSpan.hasEnded(); + } + + // if span is no longer available we consider it ended/finished + return true; + } + + @Override + public void setData(@NotNull String key, @NotNull Object value) { + data.put(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return data.get(key); + } + + @Override + public void setMeasurement(@NotNull String name, @NotNull Number value) { + if (isFinished()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "The span is already finished. Measurement %s cannot be set", + name); + return; + } + this.measurements.put(name, new MeasurementValue(value, null)); + } + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) { + if (isFinished()) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "The span is already finished. Measurement %s cannot be set", + name); + return; + } + this.measurements.put(name, new MeasurementValue(value, unit.apiName())); + } + + @Override + public boolean updateEndDate(@NotNull SentryDate date) { + if (this.finishedTimestamp != null) { + this.finishedTimestamp = date; + return true; + } + return false; + } + + @Override + public @NotNull SentryDate getStartDate() { + return startTimestamp; + } + + @Override + public @Nullable SentryDate getFinishDate() { + return finishedTimestamp; + } + + @Override + public boolean isNoOp() { + return false; + } + + @Override + public void setContext(@NotNull String key, @NotNull Object context) { + contexts.put(key, context); + } + + @Override + public @NotNull Contexts getContexts() { + return contexts; + } + + @Override + public void setTransactionName(@NotNull String name) { + setTransactionName(name, TransactionNameSource.CUSTOM); + } + + @Override + public void setTransactionName(@NotNull String name, @NotNull TransactionNameSource nameSource) { + this.transactionName = name; + this.transactionNameSource = nameSource; + } + + @Override + @ApiStatus.Internal + public @Nullable TransactionNameSource getTransactionNameSource() { + return transactionNameSource; + } + + @Override + @ApiStatus.Internal + public @Nullable String getTransactionName() { + return this.transactionName; + } + + @Override + @NotNull + public SentryId getTraceId() { + return context.getTraceId(); + } + + @Override + public @NotNull Map getData() { + return data; + } + + @Override + @NotNull + public Map getMeasurements() { + return measurements; + } + + @Override + public @Nullable Boolean isSampled() { + return context.getSampled(); + } + + @Override + public @Nullable Boolean isProfileSampled() { + return context.getProfileSampled(); + } + + @Override + public @Nullable TracesSamplingDecision getSamplingDecision() { + return context.getSamplingDecision(); + } + + @Override + @ApiStatus.Internal + public @NotNull IScopes getScopes() { + return scopes; + } + + @Override + public @NotNull Context storeInContext(Context context) { + final @Nullable ReadWriteSpan otelSpan = getSpan(); + if (otelSpan != null) { + return otelSpan.storeInContext(context); + } else { + return context; + } + } + + @SuppressWarnings("MustBeClosedChecker") + @ApiStatus.Internal + @Override + public @NotNull ISentryLifecycleToken makeCurrent() { + final @Nullable Span otelSpan = getSpan(); + if (otelSpan != null) { + final @NotNull Scope otelScope = otelSpan.makeCurrent(); + final @NotNull OtelSpanWrapperToken token = new OtelSpanWrapperToken(otelScope); + // to iterate LIFO when closing + tokensToCleanup.addFirst(token); + return token; + } + return NoOpScopesLifecycleToken.getInstance(); + } + + private static final class OtelSpanWrapperToken implements ISentryLifecycleToken { + + private final @NotNull Scope otelScope; + + OtelSpanWrapperToken(final @NotNull Scope otelScope) { + this.otelScope = otelScope; + } + + @Override + public void close() { + otelScope.close(); + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java index 14ac12323b6..ffcab9ed541 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java @@ -10,11 +10,10 @@ import io.opentelemetry.context.propagation.TextMapSetter; import io.sentry.Baggage; import io.sentry.BaggageHeader; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; +import io.sentry.ScopesAdapter; import io.sentry.SentryLevel; -import io.sentry.SentrySpanStorage; import io.sentry.SentryTraceHeader; import io.sentry.exception.InvalidSentryTraceHeaderException; import java.util.Arrays; @@ -24,19 +23,27 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * @deprecated please use {@link OtelSentryPropagator} instead + */ +@Deprecated public final class SentryPropagator implements TextMapPropagator { private static final @NotNull List FIELDS = Arrays.asList(SentryTraceHeader.SENTRY_TRACE_HEADER, BaggageHeader.BAGGAGE_HEADER); - private final @NotNull SentrySpanStorage spanStorage = SentrySpanStorage.getInstance(); - private final @NotNull IHub hub; + + @SuppressWarnings("deprecation") + private final @NotNull io.sentry.SentrySpanStorage spanStorage = + io.sentry.SentrySpanStorage.getInstance(); + + private final @NotNull IScopes scopes; public SentryPropagator() { - this(HubAdapter.getInstance()); + this(ScopesAdapter.getInstance()); } - SentryPropagator(final @NotNull IHub hub) { - this.hub = hub; + SentryPropagator(final @NotNull IScopes scopes) { + this.scopes = scopes; } @Override @@ -49,7 +56,8 @@ public void inject(final Context context, final C carrier, final TextMapSett final @NotNull Span otelSpan = Span.fromContext(context); final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); if (!otelSpanContext.isValid()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -58,7 +66,8 @@ public void inject(final Context context, final C carrier, final TextMapSett } final @Nullable ISpan sentrySpan = spanStorage.get(otelSpanContext.getSpanId()); if (sentrySpan == null || sentrySpan.isNoOp()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -106,13 +115,15 @@ public Context extract( Span wrappedSpan = Span.wrap(otelSpanContext); modifiedContext = modifiedContext.with(wrappedSpan); - hub.getOptions() + scopes + .getOptions() .getLogger() .log(SentryLevel.DEBUG, "Continuing Sentry trace %s", sentryTraceHeader.getTraceId()); return modifiedContext; } catch (InvalidSentryTraceHeaderException e) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.ERROR, diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java new file mode 100644 index 00000000000..6f35fcb9c51 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -0,0 +1,141 @@ +package io.sentry.opentelemetry; + +import static io.sentry.opentelemetry.OtelInternalSpanDetectionUtil.isSentryRequest; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.sentry.Baggage; +import io.sentry.DataCategory; +import io.sentry.IScopes; +import io.sentry.PropagationContext; +import io.sentry.SamplingContext; +import io.sentry.ScopesAdapter; +import io.sentry.SentryLevel; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanId; +import io.sentry.TracesSamplingDecision; +import io.sentry.TransactionContext; +import io.sentry.clientreport.DiscardReason; +import io.sentry.protocol.SentryId; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentrySampler implements Sampler { + + private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); + private final @NotNull IScopes scopes; + + public SentrySampler(final @NotNull IScopes scopes) { + this.scopes = scopes; + } + + public SentrySampler() { + this(ScopesAdapter.getInstance()); + } + + @Override + public SamplingResult shouldSample( + final @NotNull Context parentContext, + final @NotNull String traceId, + final @NotNull String name, + final @NotNull SpanKind spanKind, + final @NotNull Attributes attributes, + final @NotNull List parentLinks) { + if (isSentryRequest(scopes, spanKind, attributes)) { + return SamplingResult.drop(); + } + // note: parentLinks seems to usually be empty + final @Nullable Span parentOtelSpan = Span.fromContextOrNull(parentContext); + final @Nullable IOtelSpanWrapper parentSentrySpan = + parentOtelSpan != null ? spanStorage.getSentrySpan(parentOtelSpan.getSpanContext()) : null; + + if (parentSentrySpan != null) { + return copyParentSentryDecision(parentSentrySpan); + } else { + final @Nullable TracesSamplingDecision samplingDecision = + OtelSamplingUtil.extractSamplingDecision(attributes); + if (samplingDecision != null) { + return new SentrySamplingResult(samplingDecision); + } else { + return handleRootOtelSpan(traceId, parentContext); + } + } + } + + private @NotNull SamplingResult handleRootOtelSpan( + final @NotNull String traceId, final @NotNull Context parentContext) { + if (!scopes.getOptions().isTracingEnabled()) { + return SamplingResult.create(SamplingDecision.RECORD_ONLY); + } + @Nullable Baggage baggage = null; + @Nullable + SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); + @Nullable Baggage baggageFromContext = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); + if (sentryTraceHeader != null) { + baggage = baggageFromContext; + } + + // there's no way to get the span id here, so we just use a random id for sampling + SpanId randomSpanId = new SpanId(); + final @NotNull PropagationContext propagationContext = + sentryTraceHeader == null + ? new PropagationContext(new SentryId(traceId), randomSpanId, null, baggage, null) + : PropagationContext.fromHeaders(sentryTraceHeader, baggage, randomSpanId); + + final @NotNull TransactionContext transactionContext = + TransactionContext.fromPropagationContext(propagationContext); + final @NotNull TracesSamplingDecision sentryDecision = + scopes + .getOptions() + .getInternalTracesSampler() + .sample(new SamplingContext(transactionContext, null)); + + if (!sentryDecision.getSampled()) { + scopes + .getOptions() + .getClientReportRecorder() + .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Transaction); + scopes + .getOptions() + .getClientReportRecorder() + .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Span); + } + + return new SentrySamplingResult(sentryDecision); + } + + private @NotNull SentrySamplingResult copyParentSentryDecision( + final @NotNull IOtelSpanWrapper parentSentrySpan) { + final @Nullable TracesSamplingDecision parentSamplingDecision = + parentSentrySpan.getSamplingDecision(); + if (parentSamplingDecision != null) { + if (!parentSamplingDecision.getSampled()) { + scopes + .getOptions() + .getClientReportRecorder() + .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Span); + } + return new SentrySamplingResult(parentSamplingDecision); + } else { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Encountered a missing parent sampling decision where one was expected."); + return new SentrySamplingResult(new TracesSamplingDecision(true)); + } + } + + @Override + public String getDescription() { + return "SentrySampler"; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java new file mode 100644 index 00000000000..69acf521346 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySamplingResult.java @@ -0,0 +1,40 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import io.sentry.TracesSamplingDecision; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class SentrySamplingResult implements SamplingResult { + private final TracesSamplingDecision sentryDecision; + + public SentrySamplingResult(final @NotNull TracesSamplingDecision sentryDecision) { + this.sentryDecision = sentryDecision; + } + + @Override + public SamplingDecision getDecision() { + if (sentryDecision.getSampled()) { + return SamplingDecision.RECORD_AND_SAMPLE; + } else { + return SamplingDecision.RECORD_ONLY; + } + } + + @Override + public Attributes getAttributes() { + return Attributes.builder() + .put(InternalSemanticAttributes.SAMPLED, sentryDecision.getSampled()) + .put(InternalSemanticAttributes.SAMPLE_RATE, sentryDecision.getSampleRate()) + .put(InternalSemanticAttributes.PROFILE_SAMPLED, sentryDecision.getProfileSampled()) + .put(InternalSemanticAttributes.PROFILE_SAMPLE_RATE, sentryDecision.getProfileSampleRate()) + .build(); + } + + public TracesSamplingDecision getSentryDecision() { + return sentryDecision; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java new file mode 100644 index 00000000000..7851f07550c --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -0,0 +1,515 @@ +package io.sentry.opentelemetry; + +import static io.sentry.TransactionContext.DEFAULT_TRANSACTION_NAME; +import static io.sentry.opentelemetry.InternalSemanticAttributes.IS_REMOTE_PARENT; +import static io.sentry.opentelemetry.OtelInternalSpanDetectionUtil.isSentryRequest; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.incubating.ProcessIncubatingAttributes; +import io.sentry.Baggage; +import io.sentry.DateUtils; +import io.sentry.DefaultSpanFactory; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.Instrumenter; +import io.sentry.ScopesAdapter; +import io.sentry.SentryDate; +import io.sentry.SentryInstantDate; +import io.sentry.SentryLevel; +import io.sentry.SentryLongDate; +import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentrySpanExporter implements SpanExporter { + private volatile boolean stopped = false; + private final List finishedSpans = new CopyOnWriteArrayList<>(); + private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); + private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor = + new SpanDescriptionExtractor(); + private final @NotNull IScopes scopes; + + private final @NotNull List attributeKeysToRemove = + Arrays.asList( + InternalSemanticAttributes.IS_REMOTE_PARENT.getKey(), + InternalSemanticAttributes.BAGGAGE.getKey(), + InternalSemanticAttributes.BAGGAGE_MUTABLE.getKey(), + InternalSemanticAttributes.SAMPLED.getKey(), + InternalSemanticAttributes.SAMPLE_RATE.getKey(), + InternalSemanticAttributes.PROFILE_SAMPLED.getKey(), + InternalSemanticAttributes.PROFILE_SAMPLE_RATE.getKey(), + InternalSemanticAttributes.PARENT_SAMPLED.getKey(), + ProcessIncubatingAttributes.PROCESS_COMMAND_ARGS.getKey() // can be very long + ); + private static final @NotNull Long SPAN_TIMEOUT = DateUtils.secondsToNanos(5 * 60); + + public static final String TRACE_ORIGIN = "auto.opentelemetry"; + + public SentrySpanExporter() { + this(ScopesAdapter.getInstance()); + } + + public SentrySpanExporter(final @NotNull IScopes scopes) { + this.scopes = scopes; + } + + @Override + public CompletableResultCode export(Collection spans) { + if (stopped) { + // TODO unsure if there's a way to attach a message + return CompletableResultCode.ofFailure(); + } + + final int openSpanCount = finishedSpans.size(); + final int newSpanCount = spans.size(); + + final @NotNull List nonSentryRequestSpans = filterOutSentrySpans(spans); + + finishedSpans.addAll(nonSentryRequestSpans); + final @NotNull List remaining = maybeSend(finishedSpans); + final int remainingSpanCount = remaining.size(); + final int sentSpanCount = openSpanCount + newSpanCount - remainingSpanCount; + + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "SpanExporter exported %s spans, %s unset spans remaining.", + sentSpanCount, + remainingSpanCount); + + this.finishedSpans.clear(); + + final @NotNull SentryInstantDate now = new SentryInstantDate(); + + final @NotNull List nonExpired = + remaining.stream().filter((span) -> !isSpanTooOld(span, now)).collect(Collectors.toList()); + + this.finishedSpans.addAll(nonExpired); + + // TODO + + return CompletableResultCode.ofSuccess(); + } + + private boolean isSpanTooOld(final @NotNull SpanData span, final @NotNull SentryInstantDate now) { + final @NotNull SentryDate startDate = new SentryLongDate(span.getStartEpochNanos()); + boolean isTimedOut = now.diff(startDate) > SPAN_TIMEOUT; + if (isTimedOut) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Dropping span %s as it was pending for too long.", + span.getSpanId()); + } + return isTimedOut; + } + + private @NotNull List filterOutSentrySpans(final @NotNull Collection spans) { + return spans.stream() + .filter((span) -> !isSentryRequest(scopes, span.getKind(), span.getAttributes())) + .collect(Collectors.toList()); + } + + private List maybeSend(final @NotNull List spans) { + final @NotNull List grouped = groupSpansWithParents(spans); + final @NotNull List remaining = new CopyOnWriteArrayList<>(grouped); + final @NotNull List rootNodes = findCompletedRootNodes(grouped); + + for (final @NotNull SpanNode rootNode : rootNodes) { + remaining.remove(rootNode); + final @Nullable SpanData span = rootNode.getSpan(); + if (span == null) { + // TODO log + continue; + } + final @Nullable ITransaction transaction = createTransactionForOtelSpan(span); + if (transaction == null) { + // TODO log + continue; + } + + for (final @NotNull SpanNode childNode : rootNode.getChildren()) { + createAndFinishSpanForOtelSpan(childNode, transaction, remaining); + } + + transaction.finish( + mapOtelStatus(span, transaction), new SentryLongDate(span.getEndEpochNanos())); + } + + return remaining.stream() + .map((node) -> node.getSpan()) + .filter((it) -> it != null) + .collect(Collectors.toList()); + } + + private void createAndFinishSpanForOtelSpan( + final @NotNull SpanNode spanNode, + final @NotNull ISpan parentSentrySpan, + final @NotNull List remaining) { + remaining.remove(spanNode); + final @Nullable SpanData spanData = spanNode.getSpan(); + + // If this span should be dropped, we still want to create spans for the children of this + if (spanData == null) { + for (SpanNode childNode : spanNode.getChildren()) { + createAndFinishSpanForOtelSpan(childNode, parentSentrySpan, remaining); + } + return; + } + + final @NotNull String spanId = spanData.getSpanId(); + final @Nullable IOtelSpanWrapper sentrySpanMaybe = + spanStorage.getSentrySpan(spanData.getSpanContext()); + final @NotNull OtelSpanInfo spanInfo = + spanDescriptionExtractor.extractSpanInfo(spanData, sentrySpanMaybe); + + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Creating Sentry child span for OpenTelemetry span %s (trace %s). Parent span is %s.", + spanId, + spanData.getTraceId(), + spanData.getParentSpanId()); + final @NotNull SentryDate startDate = new SentryLongDate(spanData.getStartEpochNanos()); + final @NotNull SpanOptions spanOptions = new SpanOptions(); + final @NotNull io.sentry.SpanContext spanContext = + parentSentrySpan + .getSpanContext() + .copyForChild( + spanInfo.getOp(), + parentSentrySpan.getSpanContext().getSpanId(), + new SpanId(spanId)); + spanContext.setDescription(spanInfo.getDescription()); + spanContext.setInstrumenter(Instrumenter.SENTRY); + if (sentrySpanMaybe != null) { + spanContext.setSamplingDecision(sentrySpanMaybe.getSamplingDecision()); + spanOptions.setOrigin(sentrySpanMaybe.getSpanContext().getOrigin()); + } else { + spanOptions.setOrigin(TRACE_ORIGIN); + } + + spanOptions.setStartTimestamp(startDate); + + final @NotNull ISpan sentryChildSpan = parentSentrySpan.startChild(spanContext, spanOptions); + + for (Map.Entry dataField : + toMapWithStringKeys(spanData.getAttributes()).entrySet()) { + sentryChildSpan.setData(dataField.getKey(), dataField.getValue()); + } + + setOtelInstrumentationInfo(spanData, sentryChildSpan); + setOtelSpanKind(spanData, sentryChildSpan); + transferSpanDetails(sentrySpanMaybe, sentryChildSpan); + + for (SpanNode childNode : spanNode.getChildren()) { + createAndFinishSpanForOtelSpan(childNode, sentryChildSpan, remaining); + } + + sentryChildSpan.finish( + mapOtelStatus(spanData, sentryChildSpan), new SentryLongDate(spanData.getEndEpochNanos())); + } + + private void transferSpanDetails( + final @Nullable IOtelSpanWrapper sourceSpanMaybe, final @NotNull ISpan targetSpan) { + if (sourceSpanMaybe != null) { + final @NotNull IOtelSpanWrapper sourceSpan = sourceSpanMaybe; + + final @NotNull Contexts contexts = sourceSpan.getContexts(); + targetSpan.getContexts().putAll(contexts); + + final @NotNull Map data = sourceSpan.getData(); + for (Map.Entry entry : data.entrySet()) { + targetSpan.setData(entry.getKey(), entry.getValue()); + } + + final @NotNull Map tags = sourceSpan.getTags(); + for (Map.Entry entry : tags.entrySet()) { + targetSpan.setTag(entry.getKey(), entry.getValue()); + } + + targetSpan.setStatus(sourceSpan.getStatus()); + } + } + + private @Nullable ITransaction createTransactionForOtelSpan(final @NotNull SpanData span) { + final @NotNull String spanId = span.getSpanId(); + final @NotNull String traceId = span.getTraceId(); + final @Nullable IOtelSpanWrapper sentrySpanMaybe = + spanStorage.getSentrySpan(span.getSpanContext()); + final @Nullable IScopes scopesMaybe = + sentrySpanMaybe != null ? sentrySpanMaybe.getScopes() : null; + final @NotNull IScopes scopesToUse = + scopesMaybe == null ? ScopesAdapter.getInstance() : scopesMaybe; + final @NotNull OtelSpanInfo spanInfo = + spanDescriptionExtractor.extractSpanInfo(span, sentrySpanMaybe); + + scopesToUse + .getOptions() + .getLogger() + .log( + SentryLevel.DEBUG, + "Creating Sentry transaction for OpenTelemetry span %s (trace %s).", + spanId, + traceId); + final SpanId sentrySpanId = new SpanId(spanId); + + @Nullable String transactionName = spanInfo.getDescription(); + @NotNull TransactionNameSource transactionNameSource = spanInfo.getTransactionNameSource(); + @Nullable SpanId parentSpanId = null; + @Nullable Baggage baggage = null; + + if (sentrySpanMaybe != null) { + final @NotNull IOtelSpanWrapper sentrySpan = sentrySpanMaybe; + final @Nullable String transactionNameMaybe = sentrySpan.getTransactionName(); + if (transactionNameMaybe != null) { + transactionName = transactionNameMaybe; + } + final @Nullable TransactionNameSource transactionNameSourceMaybe = + sentrySpan.getTransactionNameSource(); + if (transactionNameSourceMaybe != null) { + transactionNameSource = transactionNameSourceMaybe; + } + final @NotNull SpanContext spanContext = sentrySpan.getSpanContext(); + parentSpanId = spanContext.getParentSpanId(); + baggage = spanContext.getBaggage(); + } + + final @NotNull TransactionContext transactionContext = + new TransactionContext(new SentryId(traceId), sentrySpanId, parentSpanId, null, baggage); + + TransactionOptions transactionOptions = new TransactionOptions(); + + transactionContext.setName( + transactionName == null ? DEFAULT_TRANSACTION_NAME : transactionName); + transactionContext.setTransactionNameSource(transactionNameSource); + transactionContext.setOperation(spanInfo.getOp()); + transactionContext.setInstrumenter(Instrumenter.SENTRY); + if (sentrySpanMaybe != null) { + transactionContext.setSamplingDecision(sentrySpanMaybe.getSamplingDecision()); + transactionOptions.setOrigin(sentrySpanMaybe.getSpanContext().getOrigin()); + } + + transactionOptions.setStartTimestamp(new SentryLongDate(span.getStartEpochNanos())); + transactionOptions.setSpanFactory(new DefaultSpanFactory()); + + ITransaction sentryTransaction = + scopesToUse.startTransaction(transactionContext, transactionOptions); + + final @NotNull Map otelContext = toOtelContext(span); + sentryTransaction.setContext("otel", otelContext); + + setOtelInstrumentationInfo(span, sentryTransaction); + setOtelSpanKind(span, sentryTransaction); + transferSpanDetails(sentrySpanMaybe, sentryTransaction); + + return sentryTransaction; + } + + private List findCompletedRootNodes(final @NotNull List grouped) { + final @NotNull Predicate isRootPredicate = + (node) -> { + return node.getParentNode() == null && node.getSpan() != null; + }; + return grouped.stream().filter(isRootPredicate).collect(Collectors.toList()); + } + + private List groupSpansWithParents(final @NotNull List spans) { + final @NotNull Map nodeMap = new HashMap<>(); + + for (final @NotNull SpanData spanData : spans) { + createOrUpdateSpanNodeAndRefs(nodeMap, spanData); + } + + return nodeMap.values().stream().collect(Collectors.toList()); + } + + private void createOrUpdateSpanNodeAndRefs( + final @NotNull Map nodeMap, final @NotNull SpanData spanData) { + final @NotNull String spanId = spanData.getSpanId(); + final String parentId = getParentId(spanData); + if (parentId == null) { + createOrUpdateNode(nodeMap, spanId, spanData, null, null); + return; + } + + final @NotNull SpanNode parentNode = createOrGetParentNode(nodeMap, parentId); + final @NotNull SpanNode spanNode = + createOrUpdateNode(nodeMap, spanId, spanData, null, parentNode); + parentNode.addChild(spanNode); + } + + private @Nullable String getParentId(final @NotNull SpanData spanData) { + final @NotNull String parentSpanId = spanData.getParentSpanId(); + final @Nullable Boolean isRemoteParent = spanData.getAttributes().get(IS_REMOTE_PARENT); + if (isRemoteParent != null && isRemoteParent) { + return null; + } + if (io.opentelemetry.api.trace.SpanId.isValid(parentSpanId)) { + return parentSpanId; + } + return null; + } + + private @NotNull SpanNode createOrGetParentNode( + final @NotNull Map nodeMap, final @NotNull String spanId) { + final @Nullable SpanNode existingNode = nodeMap.get(spanId); + + if (existingNode == null) { + return createOrUpdateNode(nodeMap, spanId, null, null, null); + } + + return existingNode; + } + + // TODO do we ever pass children? + private @NotNull SpanNode createOrUpdateNode( + final @NotNull Map nodeMap, + final @NotNull String spanId, + final @Nullable SpanData spanData, + final @Nullable List children, + final @Nullable SpanNode parentNode) { + final @Nullable SpanNode existingNode = nodeMap.get(spanId); + + if (existingNode != null) { + final @Nullable SpanData existingNodeSpan = existingNode.getSpan(); + + if (existingNodeSpan != null) { + // If span is already set, nothing to do here + return existingNode; + } + + // If span is not set yet, we update it + existingNode.setSpan(spanData); + existingNode.setParentNode(parentNode); + + return existingNode; + } + + final @NotNull SpanNode spanNode = new SpanNode(spanId); + spanNode.setSpan(spanData); + spanNode.setParentNode(parentNode); + spanNode.addChildren(children); + + nodeMap.put(spanId, spanNode); + + return spanNode; + } + + @SuppressWarnings("deprecation") + private SpanStatus mapOtelStatus( + final @NotNull SpanData otelSpanData, final @NotNull ISpan sentrySpan) { + final @Nullable SpanStatus existingStatus = sentrySpan.getStatus(); + if (existingStatus != null && existingStatus != SpanStatus.UNKNOWN_ERROR) { + return existingStatus; + } + + final @NotNull StatusData otelStatus = otelSpanData.getStatus(); + final @NotNull StatusCode otelStatusCode = otelStatus.getStatusCode(); + + if (StatusCode.OK.equals(otelStatusCode) || StatusCode.UNSET.equals(otelStatusCode)) { + return SpanStatus.OK; + } + + final @Nullable Long httpStatus = + otelSpanData.getAttributes().get(HttpAttributes.HTTP_RESPONSE_STATUS_CODE); + if (httpStatus != null) { + final @Nullable SpanStatus spanStatus = SpanStatus.fromHttpStatusCode(httpStatus.intValue()); + if (spanStatus != null) { + return spanStatus; + } + } + + return SpanStatus.UNKNOWN_ERROR; + } + + private @NotNull Map toOtelContext(final @NotNull SpanData spanData) { + final @NotNull Map context = new HashMap<>(); + + context.put("attributes", toMapWithStringKeys(spanData.getAttributes())); + context.put("resource", toMapWithStringKeys(spanData.getResource().getAttributes())); + + return context; + } + + private @NotNull Map toMapWithStringKeys(final @Nullable Attributes attributes) { + final @NotNull Map mapWithStringKeys = new HashMap<>(); + + if (attributes != null) { + attributes.forEach( + (key, value) -> { + if (key != null) { + final @NotNull String stringKey = key.getKey(); + if (!shouldRemoveAttribute(stringKey)) { + mapWithStringKeys.put(stringKey, value); + } + } + }); + } + + return mapWithStringKeys; + } + + private boolean shouldRemoveAttribute(final @NotNull String key) { + return attributeKeysToRemove.contains(key); + } + + private void setOtelInstrumentationInfo( + final @NotNull SpanData span, final @NotNull ISpan sentryTransaction) { + final @Nullable String otelInstrumentationName = span.getInstrumentationScopeInfo().getName(); + if (otelInstrumentationName != null) { + sentryTransaction.setData("otel.instrumentation.name", otelInstrumentationName); + } + + final @Nullable String otelInstrumentationVersion = + span.getInstrumentationScopeInfo().getVersion(); + if (otelInstrumentationVersion != null) { + sentryTransaction.setData("otel.instrumentation.version", otelInstrumentationVersion); + } + } + + private void setOtelSpanKind(final @NotNull SpanData otelSpan, final @NotNull ISpan sentrySpan) { + sentrySpan.setData("otel.kind", otelSpan.getKind().name()); + } + + @Override + public CompletableResultCode flush() { + scopes.flush(10000); + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + stopped = true; + scopes.close(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java index a9e70f66a06..2b650ef9dd2 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java @@ -1,5 +1,7 @@ package io.sentry.opentelemetry; +import static io.sentry.TransactionContext.DEFAULT_TRANSACTION_NAME; + import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; @@ -10,21 +12,22 @@ import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; -import io.opentelemetry.semconv.SemanticAttributes; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; import io.sentry.Baggage; import io.sentry.DsnUtil; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.ITransaction; import io.sentry.Instrumenter; import io.sentry.PropagationContext; +import io.sentry.ScopesAdapter; import io.sentry.SentryDate; import io.sentry.SentryLevel; import io.sentry.SentryLongDate; -import io.sentry.SentrySpanStorage; import io.sentry.SentryTraceHeader; import io.sentry.SpanId; +import io.sentry.SpanOptions; import io.sentry.SpanStatus; import io.sentry.TransactionContext; import io.sentry.TransactionOptions; @@ -37,6 +40,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * @deprecated please use {@link OtelSentrySpanProcessor} instead. + */ +@Deprecated public final class SentrySpanProcessor implements SpanProcessor { private static final String TRACE_ORIGN = "auto.otel"; @@ -45,15 +52,19 @@ public final class SentrySpanProcessor implements SpanProcessor { Arrays.asList(SpanKind.CLIENT, SpanKind.INTERNAL); private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor = new SpanDescriptionExtractor(); - private final @NotNull SentrySpanStorage spanStorage = SentrySpanStorage.getInstance(); - private final @NotNull IHub hub; + + @SuppressWarnings("deprecation") + private final @NotNull io.sentry.SentrySpanStorage spanStorage = + io.sentry.SentrySpanStorage.getInstance(); + + private final @NotNull IScopes scopes; public SentrySpanProcessor() { - this(HubAdapter.getInstance()); + this(ScopesAdapter.getInstance()); } - SentrySpanProcessor(final @NotNull IHub hub) { - this.hub = hub; + SentrySpanProcessor(final @NotNull IScopes scopes) { + this.scopes = scopes; } @Override @@ -65,7 +76,8 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri final @NotNull TraceData traceData = getTraceData(otelSpan, parentContext); if (isSentryRequest(otelSpan)) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -78,7 +90,8 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri traceData.getParentSpanId() == null ? null : spanStorage.get(traceData.getParentSpanId()); if (sentryParentSpan != null) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -88,13 +101,15 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri traceData.getParentSpanId()); final @NotNull SentryDate startDate = new SentryLongDate(otelSpan.toSpanData().getStartEpochNanos()); + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGN); final @NotNull ISpan sentryChildSpan = sentryParentSpan.startChild( - otelSpan.getName(), otelSpan.getName(), startDate, Instrumenter.OTEL); - sentryChildSpan.getSpanContext().setOrigin(TRACE_ORIGN); + otelSpan.getName(), otelSpan.getName(), startDate, Instrumenter.OTEL, spanOptions); spanStorage.store(traceData.getSpanId(), sentryChildSpan); } else { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -122,9 +137,9 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setStartTimestamp( new SentryLongDate(otelSpan.toSpanData().getStartEpochNanos())); + transactionOptions.setOrigin(TRACE_ORIGN); - ISpan sentryTransaction = hub.startTransaction(transactionContext, transactionOptions); - sentryTransaction.getSpanContext().setOrigin(TRACE_ORIGN); + ISpan sentryTransaction = scopes.startTransaction(transactionContext, transactionOptions); spanStorage.store(traceData.getSpanId(), sentryTransaction); } } @@ -144,7 +159,8 @@ public void onEnd(final @NotNull ReadableSpan otelSpan) { final @Nullable ISpan sentrySpan = spanStorage.removeAndGet(traceData.getSpanId()); if (sentrySpan == null) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -155,7 +171,8 @@ public void onEnd(final @NotNull ReadableSpan otelSpan) { } if (isSentryRequest(otelSpan)) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -168,7 +185,8 @@ public void onEnd(final @NotNull ReadableSpan otelSpan) { if (sentrySpan instanceof ITransaction) { final @NotNull ITransaction sentryTransaction = (ITransaction) sentrySpan; updateTransactionWithOtelData(sentryTransaction, otelSpan); - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -178,7 +196,8 @@ public void onEnd(final @NotNull ReadableSpan otelSpan) { traceData.getTraceId()); } else { updateSpanWithOtelData(sentrySpan, otelSpan); - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -201,7 +220,8 @@ public boolean isEndRequired() { private boolean ensurePrerequisites(final @NotNull ReadableSpan otelSpan) { if (!hasSentryBeenInitialized()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -209,9 +229,10 @@ private boolean ensurePrerequisites(final @NotNull ReadableSpan otelSpan) { return false; } - final @NotNull Instrumenter instrumenter = hub.getOptions().getInstrumenter(); + final @NotNull Instrumenter instrumenter = scopes.getOptions().getInstrumenter(); if (!Instrumenter.OTEL.equals(instrumenter)) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -222,7 +243,8 @@ private boolean ensurePrerequisites(final @NotNull ReadableSpan otelSpan) { final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); if (!otelSpanContext.isValid()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -240,8 +262,8 @@ private boolean isSentryRequest(final @NotNull ReadableSpan otelSpan) { return false; } - final @Nullable String httpUrl = otelSpan.getAttribute(SemanticAttributes.HTTP_URL); - return DsnUtil.urlContainsDsnHost(hub.getOptions(), httpUrl); + final @Nullable String httpUrl = otelSpan.getAttribute(UrlAttributes.URL_FULL); + return DsnUtil.urlContainsDsnHost(scopes.getOptions(), httpUrl); } private @NotNull TraceData getTraceData( @@ -272,10 +294,12 @@ private boolean isSentryRequest(final @NotNull ReadableSpan otelSpan) { private void updateTransactionWithOtelData( final @NotNull ITransaction sentryTransaction, final @NotNull ReadableSpan otelSpan) { final @NotNull OtelSpanInfo otelSpanInfo = - spanDescriptionExtractor.extractSpanDescription(otelSpan); + spanDescriptionExtractor.extractSpanInfo(otelSpan.toSpanData(), null); sentryTransaction.setOperation(otelSpanInfo.getOp()); + String transactionName = otelSpanInfo.getDescription(); sentryTransaction.setName( - otelSpanInfo.getDescription(), otelSpanInfo.getTransactionNameSource()); + transactionName == null ? DEFAULT_TRANSACTION_NAME : transactionName, + otelSpanInfo.getTransactionNameSource()); final @NotNull Map otelContext = toOtelContext(otelSpan); sentryTransaction.setContext("otel", otelContext); @@ -307,7 +331,7 @@ private void updateSpanWithOtelData( }); final @NotNull OtelSpanInfo otelSpanInfo = - spanDescriptionExtractor.extractSpanDescription(otelSpan); + spanDescriptionExtractor.extractSpanInfo(otelSpan.toSpanData(), null); sentrySpan.setOperation(otelSpanInfo.getOp()); sentrySpan.setDescription(otelSpanInfo.getDescription()); } @@ -322,7 +346,8 @@ private SpanStatus mapOtelStatus(final @NotNull ReadableSpan otelSpan) { return SpanStatus.OK; } - final @Nullable Long httpStatus = otelSpan.getAttribute(SemanticAttributes.HTTP_STATUS_CODE); + final @Nullable Long httpStatus = + otelSpan.getAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE); if (httpStatus != null) { final @Nullable SpanStatus spanStatus = SpanStatus.fromHttpStatusCode(httpStatus.intValue()); if (spanStatus != null) { @@ -334,7 +359,7 @@ private SpanStatus mapOtelStatus(final @NotNull ReadableSpan otelSpan) { } private boolean hasSentryBeenInitialized() { - return hub.isEnabled(); + return scopes.isEnabled(); } private @NotNull Map toMapWithStringKeys(final @Nullable Attributes attributes) { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java index 57db007b0a8..2047bd37f80 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java @@ -1,8 +1,12 @@ package io.sentry.opentelemetry; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.sdk.trace.ReadableSpan; -import io.opentelemetry.semconv.SemanticAttributes; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.opentelemetry.semconv.incubating.DbIncubatingAttributes; +import io.opentelemetry.semconv.incubating.HttpIncubatingAttributes; import io.sentry.protocol.TransactionNameSource; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -12,39 +16,71 @@ public final class SpanDescriptionExtractor { @SuppressWarnings("deprecation") - public @NotNull OtelSpanInfo extractSpanDescription(final @NotNull ReadableSpan otelSpan) { - final @NotNull String name = otelSpan.getName(); + public @NotNull OtelSpanInfo extractSpanInfo( + final @NotNull SpanData otelSpan, final @Nullable IOtelSpanWrapper sentrySpan) { + if (!isInternalSpanKind(otelSpan)) { + final @NotNull Attributes attributes = otelSpan.getAttributes(); - final @Nullable String httpMethod = otelSpan.getAttribute(SemanticAttributes.HTTP_METHOD); - if (httpMethod != null) { - return descriptionForHttpMethod(otelSpan, httpMethod); - } + final @Nullable String httpMethod = attributes.get(HttpAttributes.HTTP_REQUEST_METHOD); + if (httpMethod != null) { + return descriptionForHttpMethod(otelSpan, httpMethod); + } - final @Nullable String dbSystem = otelSpan.getAttribute(SemanticAttributes.DB_SYSTEM); - if (dbSystem != null) { - return descriptionForDbSystem(otelSpan); + final @Nullable String httpRequestMethod = attributes.get(HttpAttributes.HTTP_REQUEST_METHOD); + if (httpRequestMethod != null) { + return descriptionForHttpMethod(otelSpan, httpRequestMethod); + } + + final @Nullable String dbSystem = attributes.get(DbIncubatingAttributes.DB_SYSTEM); + if (dbSystem != null) { + return descriptionForDbSystem(otelSpan); + } } - return new OtelSpanInfo(name, name, TransactionNameSource.CUSTOM); + final @NotNull String name = otelSpan.getName(); + final @Nullable String maybeDescription = + sentrySpan != null ? sentrySpan.getDescription() : name; + final @NotNull String description = maybeDescription != null ? maybeDescription : name; + return new OtelSpanInfo(name, description, TransactionNameSource.CUSTOM); + } + + private boolean isInternalSpanKind(final @NotNull SpanData otelSpan) { + return SpanKind.INTERNAL.equals(otelSpan.getKind()); } @SuppressWarnings("deprecation") private OtelSpanInfo descriptionForHttpMethod( - final @NotNull ReadableSpan otelSpan, final @NotNull String httpMethod) { + final @NotNull SpanData otelSpan, final @NotNull String httpMethod) { final @NotNull String name = otelSpan.getName(); final @NotNull SpanKind kind = otelSpan.getKind(); final @NotNull StringBuilder opBuilder = new StringBuilder("http"); + final @NotNull Attributes attributes = otelSpan.getAttributes(); if (SpanKind.CLIENT.equals(kind)) { opBuilder.append(".client"); } else if (SpanKind.SERVER.equals(kind)) { opBuilder.append(".server"); } - final @Nullable String httpTarget = otelSpan.getAttribute(SemanticAttributes.HTTP_TARGET); - final @Nullable String httpRoute = otelSpan.getAttribute(SemanticAttributes.HTTP_ROUTE); - final @Nullable String httpPath = httpRoute != null ? httpRoute : httpTarget; + final @Nullable String httpTarget = attributes.get(HttpIncubatingAttributes.HTTP_TARGET); + final @Nullable String httpRoute = attributes.get(HttpAttributes.HTTP_ROUTE); + @Nullable String httpPath = httpRoute; + if (httpPath == null) { + httpPath = httpTarget; + } final @NotNull String op = opBuilder.toString(); + final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL); + if (urlFull != null) { + if (httpPath == null) { + httpPath = urlFull; + } + } + + final @Nullable String urlPath = attributes.get(UrlAttributes.URL_PATH); + if (httpPath == null && urlPath != null) { + httpPath = urlPath; + } + if (httpPath == null) { return new OtelSpanInfo(op, name, TransactionNameSource.CUSTOM); } @@ -56,9 +92,18 @@ private OtelSpanInfo descriptionForHttpMethod( return new OtelSpanInfo(op, description, transactionNameSource); } - private OtelSpanInfo descriptionForDbSystem(final @NotNull ReadableSpan otelSpan) { - @Nullable String dbStatement = otelSpan.getAttribute(SemanticAttributes.DB_STATEMENT); - @NotNull String description = dbStatement != null ? dbStatement : otelSpan.getName(); - return new OtelSpanInfo("db", description, TransactionNameSource.TASK); + @SuppressWarnings("deprecation") + private OtelSpanInfo descriptionForDbSystem(final @NotNull SpanData otelSpan) { + final @NotNull Attributes attributes = otelSpan.getAttributes(); + @Nullable String dbStatement = attributes.get(DbIncubatingAttributes.DB_STATEMENT); + if (dbStatement != null) { + return new OtelSpanInfo("db", dbStatement, TransactionNameSource.TASK); + } + @Nullable String dbQueryText = attributes.get(DbIncubatingAttributes.DB_QUERY_TEXT); + if (dbQueryText != null) { + return new OtelSpanInfo("db", dbQueryText, TransactionNameSource.TASK); + } + + return new OtelSpanInfo("db", otelSpan.getName(), TransactionNameSource.TASK); } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanNode.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanNode.java new file mode 100644 index 00000000000..e74747d8a1d --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanNode.java @@ -0,0 +1,56 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SpanNode { + private final @NotNull String id; + private @Nullable SpanData span; + private @Nullable SpanNode parentNode; + private @NotNull List children = new CopyOnWriteArrayList<>(); + + public SpanNode(final @NotNull String spanId) { + this.id = spanId; + } + + public @NotNull String getId() { + return id; + } + + public @Nullable SpanData getSpan() { + return span; + } + + public void setSpan(final @Nullable SpanData span) { + this.span = span; + } + + public @Nullable SpanNode getParentNode() { + return parentNode; + } + + public void setParentNode(final @Nullable SpanNode parentNode) { + this.parentNode = parentNode; + } + + public @NotNull List getChildren() { + return children; + } + + public void addChildren(final @Nullable List children) { + if (children != null) { + this.children.addAll(children); + } + } + + public void addChild(final @Nullable SpanNode child) { + if (child != null) { + this.children.add(child); + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java index 08751b56092..5904db39e77 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@Deprecated @ApiStatus.Internal public final class TraceData { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt index 5ed757ba167..053f80e5372 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt @@ -17,11 +17,12 @@ import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.trace.ReadWriteSpan import io.opentelemetry.sdk.trace.ReadableSpan import io.opentelemetry.sdk.trace.SdkTracerProvider -import io.opentelemetry.semconv.SemanticAttributes +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.UrlAttributes import io.sentry.Baggage import io.sentry.BaggageHeader import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.Instrumenter @@ -29,6 +30,7 @@ import io.sentry.SentryDate import io.sentry.SentryEvent import io.sentry.SentryOptions import io.sentry.SentryTraceHeader +import io.sentry.SpanOptions import io.sentry.SpanStatus import io.sentry.TransactionContext import io.sentry.TransactionOptions @@ -65,7 +67,7 @@ class SentrySpanProcessorTest { it.dsn = "https://key@sentry.io/proj" it.instrumenter = Instrumenter.OTEL } - val hub = mock() + val scopes = mock() val transaction = mock() val span = mock() val spanContext = mock() @@ -75,9 +77,9 @@ class SentrySpanProcessorTest { val baggage = Baggage.fromHeader(BAGGAGE_HEADER_STRING) fun setup() { - whenever(hub.isEnabled).thenReturn(true) - whenever(hub.options).thenReturn(options) - whenever(hub.startTransaction(any(), any())).thenReturn(transaction) + whenever(scopes.isEnabled).thenReturn(true) + whenever(scopes.options).thenReturn(options) + whenever(scopes.startTransaction(any(), any())).thenReturn(transaction) whenever(spanContext.operation).thenReturn("spanContextOp") whenever(spanContext.parentSpanId).thenReturn(io.sentry.SpanId("cedf5b7571cb4972")) @@ -91,10 +93,10 @@ class SentrySpanProcessorTest { whenever(span.toBaggageHeader(any())).thenReturn(baggageHeader) whenever(transaction.toBaggageHeader(any())).thenReturn(baggageHeader) - whenever(transaction.startChild(any(), anyOrNull(), anyOrNull(), eq(Instrumenter.OTEL))).thenReturn(span) + whenever(transaction.startChild(any(), anyOrNull(), anyOrNull(), eq(Instrumenter.OTEL), any())).thenReturn(span) val sdkTracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(SentrySpanProcessor(hub)) + .addSpanProcessor(SentrySpanProcessor(scopes)) .build() openTelemetry = OpenTelemetrySdk.builder() @@ -124,7 +126,7 @@ class SentrySpanProcessorTest { fun `ignores sentry client request`() { fixture.setup() givenSpanBuilder(SpanKind.CLIENT) - .setAttribute(SemanticAttributes.HTTP_URL, "https://key@sentry.io/proj/some-api") + .setAttribute(UrlAttributes.URL_FULL, "https://key@sentry.io/proj/some-api") .startSpan() thenNoTransactionIsStarted() @@ -134,7 +136,7 @@ class SentrySpanProcessorTest { fun `ignores sentry internal request`() { fixture.setup() givenSpanBuilder(SpanKind.CLIENT) - .setAttribute(SemanticAttributes.HTTP_URL, "https://key@sentry.io/proj/some-api") + .setAttribute(UrlAttributes.URL_FULL, "https://key@sentry.io/proj/some-api") .startSpan() thenNoTransactionIsStarted() @@ -146,13 +148,13 @@ class SentrySpanProcessorTest { val context = mock() val span = mock() - whenever(fixture.hub.isEnabled).thenReturn(false) + whenever(fixture.scopes.isEnabled).thenReturn(false) - SentrySpanProcessor(fixture.hub).onStart(context, span) + SentrySpanProcessor(fixture.scopes).onStart(context, span) - verify(fixture.hub).isEnabled - verify(fixture.hub).options - verifyNoMoreInteractions(fixture.hub) + verify(fixture.scopes).isEnabled + verify(fixture.scopes).options + verifyNoMoreInteractions(fixture.scopes) verifyNoInteractions(context, span) } @@ -161,13 +163,13 @@ class SentrySpanProcessorTest { fixture.setup() val span = mock() - whenever(fixture.hub.isEnabled).thenReturn(false) + whenever(fixture.scopes.isEnabled).thenReturn(false) - SentrySpanProcessor(fixture.hub).onEnd(span) + SentrySpanProcessor(fixture.scopes).onEnd(span) - verify(fixture.hub).isEnabled - verify(fixture.hub).options - verifyNoMoreInteractions(fixture.hub) + verify(fixture.scopes).isEnabled + verify(fixture.scopes).options + verifyNoMoreInteractions(fixture.scopes) verifyNoInteractions(span) } @@ -178,7 +180,7 @@ class SentrySpanProcessorTest { val mockSpanContext = mock() whenever(mockSpanContext.spanId).thenReturn(SpanId.getInvalid()) whenever(mockSpan.spanContext).thenReturn(mockSpanContext) - SentrySpanProcessor(fixture.hub).onStart(Context.current(), mockSpan) + SentrySpanProcessor(fixture.scopes).onStart(Context.current(), mockSpan) thenNoTransactionIsStarted() } @@ -190,7 +192,7 @@ class SentrySpanProcessorTest { whenever(mockSpanContext.spanId).thenReturn(SpanId.fromBytes("seed".toByteArray())) whenever(mockSpanContext.traceId).thenReturn(TraceId.getInvalid()) whenever(mockSpan.spanContext).thenReturn(mockSpanContext) - SentrySpanProcessor(fixture.hub).onStart(Context.current(), mockSpan) + SentrySpanProcessor(fixture.scopes).onStart(Context.current(), mockSpan) thenNoTransactionIsStarted() } @@ -303,8 +305,8 @@ class SentrySpanProcessorTest { thenChildSpanIsStarted() otelChildSpan.setStatus(StatusCode.ERROR) - otelChildSpan.setAttribute(SemanticAttributes.HTTP_URL, "http://github.com/getsentry/sentry-java") - otelChildSpan.setAttribute(SemanticAttributes.HTTP_STATUS_CODE, 404L) + otelChildSpan.setAttribute(UrlAttributes.URL_FULL, "http://github.com/getsentry/sentry-java") + otelChildSpan.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 404L) otelChildSpan.end() thenChildSpanIsFinished(SpanStatus.NOT_FOUND) @@ -342,7 +344,7 @@ class SentrySpanProcessorTest { thenTransactionIsStarted(otelSpan, isContinued = true) otelSpan.makeCurrent().use { _ -> - val processedEvent = OpenTelemetryLinkErrorEventProcessor(fixture.hub).process(SentryEvent(), Hint()) + val processedEvent = OpenTelemetryLinkErrorEventProcessor(fixture.scopes).process(SentryEvent(), Hint()) val traceContext = processedEvent!!.contexts.trace!! assertEquals("2722d9f6ec019ade60c776169d9a8904", traceContext.traceId.toString()) @@ -361,7 +363,7 @@ class SentrySpanProcessorTest { fixture.options.instrumenter = Instrumenter.SENTRY fixture.setup() - val processedEvent = OpenTelemetryLinkErrorEventProcessor(fixture.hub).process(SentryEvent(), Hint()) + val processedEvent = OpenTelemetryLinkErrorEventProcessor(fixture.scopes).process(SentryEvent(), Hint()) thenNoTraceContextHasBeenAddedToEvent(processedEvent) } @@ -393,7 +395,7 @@ class SentrySpanProcessorTest { private fun thenTransactionIsStarted(otelSpan: Span, isContinued: Boolean = false, continuesWithFilledBaggage: Boolean = true) { if (isContinued) { - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("testspan", it.name) assertEquals(TransactionNameSource.CUSTOM, it.transactionNameSource) @@ -423,7 +425,7 @@ class SentrySpanProcessorTest { } ) } else { - verify(fixture.hub).startTransaction( + verify(fixture.scopes).startTransaction( check { assertEquals("testspan", it.name) assertEquals(TransactionNameSource.CUSTOM, it.transactionNameSource) @@ -451,7 +453,7 @@ class SentrySpanProcessorTest { } private fun thenNoTransactionIsStarted() { - verify(fixture.hub, never()).startTransaction( + verify(fixture.scopes, never()).startTransaction( any(), any() ) @@ -462,7 +464,8 @@ class SentrySpanProcessorTest { eq("childspan"), eq("childspan"), any(), - eq(Instrumenter.OTEL) + eq(Instrumenter.OTEL), + any() ) } diff --git a/sentry-quartz/api/sentry-quartz.api b/sentry-quartz/api/sentry-quartz.api index ff32280dc5b..bb8b142a912 100644 --- a/sentry-quartz/api/sentry-quartz.api +++ b/sentry-quartz/api/sentry-quartz.api @@ -5,9 +5,10 @@ public final class io/sentry/quartz/BuildConfig { public final class io/sentry/quartz/SentryJobListener : org/quartz/JobListener { public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; + public static final field SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY Ljava/lang/String; public static final field SENTRY_SLUG_KEY Ljava/lang/String; public fun ()V - public fun (Lio/sentry/IHub;)V + public fun (Lio/sentry/IScopes;)V public fun getName ()Ljava/lang/String; public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 28a0e512005..38dbffdc8ee 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -3,11 +3,13 @@ import io.sentry.BuildConfig; import io.sentry.CheckIn; import io.sentry.CheckInStatus; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ScopesAdapter; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; +import io.sentry.util.LifecycleHelper; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import org.jetbrains.annotations.ApiStatus; @@ -23,15 +25,16 @@ public final class SentryJobListener implements JobListener { public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; public static final String SENTRY_SLUG_KEY = "sentry-slug"; + public static final String SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY = "sentry-scope-lifecycle"; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; public SentryJobListener() { - this(HubAdapter.getInstance()); + this(ScopesAdapter.getInstance()); } - public SentryJobListener(final @NotNull IHub hub) { - this.hub = Objects.requireNonNull(hub, "hub is required"); + public SentryJobListener(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz"); SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME); @@ -49,15 +52,18 @@ public void jobToBeExecuted(final @NotNull JobExecutionContext context) { if (maybeSlug == null) { return; } - hub.pushScope(); - TracingUtils.startNewTrace(hub); + final @NotNull ISentryLifecycleToken lifecycleToken = + scopes.forkedScopes("SentryJobListener").makeCurrent(); + TracingUtils.startNewTrace(scopes); final @NotNull String slug = maybeSlug; final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); - final @NotNull SentryId checkInId = hub.captureCheckIn(checkIn); + final @NotNull SentryId checkInId = scopes.captureCheckIn(checkIn); context.put(SENTRY_CHECK_IN_ID_KEY, checkInId); context.put(SENTRY_SLUG_KEY, slug); + context.put(SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY, lifecycleToken); } catch (Throwable t) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log(SentryLevel.ERROR, "Unable to capture check-in in jobToBeExecuted.", t); } @@ -94,14 +100,15 @@ public void jobWasExecuted(JobExecutionContext context, JobExecutionException jo if (slug != null) { final boolean isFailed = jobException != null; final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK; - hub.captureCheckIn(new CheckIn(checkInId, slug, status)); + scopes.captureCheckIn(new CheckIn(checkInId, slug, status)); } } catch (Throwable t) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t); } finally { - hub.popScope(); + LifecycleHelper.close(context.get(SENTRY_SCOPE_LIFECYCLE_TOKEN_KEY)); } } } diff --git a/sentry-samples/sentry-samples-android/CMakeLists.txt b/sentry-samples/sentry-samples-android/CMakeLists.txt index 03a2da3f661..8b2e39fcd2f 100644 --- a/sentry-samples/sentry-samples-android/CMakeLists.txt +++ b/sentry-samples/sentry-samples-android/CMakeLists.txt @@ -3,20 +3,16 @@ project(Sentry-Sample LANGUAGES C CXX) add_library(native-sample SHARED src/main/cpp/native-sample.cpp) -# make sure that we build it as a shared lib instead of a static lib -set(BUILD_SHARED_LIBS ON) -set(SENTRY_BUILD_SHARED_LIBS ON) - -add_subdirectory(../../sentry-android-ndk/${SENTRY_NATIVE_SRC} sentry_build) +find_package(sentry-native-ndk REQUIRED CONFIG) find_library(LOG_LIB log) target_link_libraries(native-sample PRIVATE ${LOG_LIB} - $ + sentry-native-ndk::sentry-android + sentry-native-ndk::sentry ) # Android 15: Support 16KB page sizes # see https://developer.android.com/guide/practices/page-sizes target_link_options(native-sample PRIVATE "-Wl,-z,max-page-size=16384") - diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 90c71b82891..88ee38e9311 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -9,22 +9,17 @@ android { defaultConfig { applicationId = "io.sentry.samples.android" - minSdk = Config.Android.minSdkVersionCompose + minSdk = Config.Android.minSdkVersion targetSdk = Config.Android.targetSdkVersion versionCode = 2 versionName = project.version.toString() externalNativeBuild { - val sentryNativeSrc = if (File("${project.projectDir}/../../sentry-android-ndk/sentry-native-local").exists()) { - "sentry-native-local" - } else { - "sentry-native" - } - println("sentry-samples-android: $sentryNativeSrc") - cmake { - arguments.add(0, "-DANDROID_STL=c++_static") - arguments.add(0, "-DSENTRY_NATIVE_SRC=$sentryNativeSrc") + // Android 15: As we're using an older version of AGP / NDK, the STL is not 16kb page aligned yet + // Our example code doesn't use the STL, so we simply disable it + // See https://developer.android.com/guide/practices/page-sizes + arguments.add(0, "-DANDROID_STL=none") } } @@ -38,6 +33,7 @@ android { // Note that the viewBinding.enabled property is now deprecated. viewBinding = true compose = true + prefab = true } composeOptions { @@ -114,11 +110,11 @@ dependencies { implementation(kotlin(Config.kotlinStdLib, org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION)) implementation(projects.sentryAndroid) - implementation(projects.sentryAndroidOkhttp) implementation(projects.sentryAndroidFragment) implementation(projects.sentryAndroidTimber) implementation(projects.sentryCompose) implementation(projects.sentryComposeHelper) + implementation(projects.sentryOkhttp) implementation(Config.Libs.fragment) implementation(Config.Libs.timber) @@ -140,6 +136,7 @@ dependencies { implementation(Config.Libs.composeNavigation) implementation(Config.Libs.composeMaterial) implementation(Config.Libs.composeCoil) + implementation(Config.Libs.sentryNativeNdk) debugImplementation(Config.Libs.leakCanary) } diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index d8ae6c709d9..84372b7766d 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -66,9 +66,6 @@ - - @@ -108,15 +105,14 @@ - - - - + + + - + @@ -166,8 +162,6 @@ - - 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 33dd35f9867..da52c72a68d 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 @@ -254,9 +254,6 @@ public void run() { binding.openFrameDataForSpans.setOnClickListener( view -> startActivity(new Intent(this, FrameDataForSpansActivity.class))); - binding.openMetrics.setOnClickListener( - view -> startActivity(new Intent(this, MetricsActivity.class))); - setContentView(binding.getRoot()); } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MetricsActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MetricsActivity.kt deleted file mode 100644 index ebc535488a3..00000000000 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MetricsActivity.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.sentry.samples.android - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.sentry.Sentry -import kotlin.random.Random - -class MetricsActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - MaterialTheme { - Surface { - Column( - modifier = Modifier.padding(20.dp) - ) { - Button(onClick = { - Sentry.metrics().increment("example.increment") - }) { - Text(text = "Increment") - } - Button(onClick = { - Sentry.metrics().distribution("example.distribution", Random.nextDouble()) - }) { - Text(text = "Distribution") - } - Button(onClick = { - Sentry.metrics().gauge("example.gauge", Random.nextDouble()) - }) { - Text(text = "Gauge") - } - Button(onClick = { - Sentry.metrics().set("example.set", Random.nextInt()) - }) { - Text(text = "Set") - } - } - } - } - } - } -} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 9a3169fef05..a4a1c5397a9 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,7 +2,6 @@ import android.app.Application; import android.os.StrictMode; -import io.sentry.Sentry; /** Apps. main Application. */ public class MyApplication extends Application { @@ -25,8 +24,6 @@ public void onCreate() { // }); // */ // }); - - Sentry.metrics().increment("app.start.cold"); } private void strictMode() { diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt index 610fc1534d4..64e3f484410 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/ProfilingActivity.kt @@ -75,7 +75,7 @@ class ProfilingActivity : AppCompatActivity() { private fun finishTransactionAndPrintResults(t: ITransaction) { t.finish() profileFinished = true - val profilesDirPath = Sentry.getCurrentHub().options.profilingTracesDirPath + val profilesDirPath = Sentry.getCurrentScopes().options.profilingTracesDirPath if (profilesDirPath == null) { Toast.makeText(this, R.string.profiling_no_dir_set, Toast.LENGTH_SHORT).show() return @@ -84,7 +84,7 @@ class ProfilingActivity : AppCompatActivity() { // We have concurrent profiling now. We have to wait for all transactions to finish (e.g. button click) // before reading the profile, otherwise it's empty and a crash occurs if (Sentry.getSpan() != null) { - val timeout = Sentry.getCurrentHub().options.idleTimeout ?: 0 + val timeout = Sentry.getCurrentScopes().options.idleTimeout ?: 0 val duration = (getProfileDuration() * 1000).toLong() Thread.sleep((timeout - duration).coerceAtLeast(0)) } @@ -100,7 +100,7 @@ class ProfilingActivity : AppCompatActivity() { val traceData = ProfilingTraceData(profile, t) // Create envelope item from copied profile val item = - SentryEnvelopeItem.fromProfilingTrace(traceData, Long.MAX_VALUE, Sentry.getCurrentHub().options.serializer) + SentryEnvelopeItem.fromProfilingTrace(traceData, Long.MAX_VALUE, Sentry.getCurrentScopes().options.serializer) val itemData = item.data // Compress the envelope item using Gzip 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 620acaa04cf..6fb8d028637 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 @@ -142,11 +142,6 @@ android:layout_height="wrap_content" android:text="@string/open_frame_data_for_spans"/> -

* - * that clones the Hub before execution and restores it afterwards. This prevents reused threads - * (e.g. from thread-pools) from getting an incorrect state. + * that forks the current scope(s) before execution and restores previous state afterwards. Which + * scope(s) are forked, depends on the method used here. This prevents reused threads (e.g. from + * thread-pools) from getting an incorrect state. */ public final class SentryWrapper { /** * Helper method to wrap {@link Callable} * - *

Clones the Hub before execution and restores it afterwards. This prevents reused threads - * (e.g. from thread-pools) from getting an incorrect state. + *

Forks current and isolation scope before execution and restores previous state afterwards. + * This prevents reused threads (e.g. from thread-pools) from getting an incorrect state. * * @param callable - the {@link Callable} to be wrapped * @return the wrapped {@link Callable} * @param - the result type of the {@link Callable} */ public static Callable wrapCallable(final @NotNull Callable callable) { - final IHub newHub = Sentry.getCurrentHub().clone(); + final IScopes newScopes = Sentry.getCurrentScopes().forkedScopes("SentryWrapper.wrapCallable"); return () -> { - final IHub oldState = Sentry.getCurrentHub(); - Sentry.setCurrentHub(newHub); - try { + try (ISentryLifecycleToken ignored = newScopes.makeCurrent()) { return callable.call(); - } finally { - Sentry.setCurrentHub(oldState); } }; } @@ -44,24 +41,19 @@ public static Callable wrapCallable(final @NotNull Callable callable) /** * Helper method to wrap {@link Supplier} * - *

Clones the Hub before execution and restores it afterwards. This prevents reused threads - * (e.g. from thread-pools) from getting an incorrect state. + *

Forks current and isolation scope before execution and restores previous state afterwards. + * This prevents reused threads (e.g. from thread-pools) from getting an incorrect state. * * @param supplier - the {@link Supplier} to be wrapped * @return the wrapped {@link Supplier} * @param - the result type of the {@link Supplier} */ public static Supplier wrapSupplier(final @NotNull Supplier supplier) { - - final IHub newHub = Sentry.getCurrentHub().clone(); + final IScopes newScopes = Sentry.forkedScopes("SentryWrapper.wrapSupplier"); return () -> { - final IHub oldState = Sentry.getCurrentHub(); - Sentry.setCurrentHub(newHub); - try { + try (ISentryLifecycleToken ignore = newScopes.makeCurrent()) { return supplier.get(); - } finally { - Sentry.setCurrentHub(oldState); } }; } diff --git a/sentry/src/main/java/io/sentry/Session.java b/sentry/src/main/java/io/sentry/Session.java index 482b055b676..3ce2d70e89e 100644 --- a/sentry/src/main/java/io/sentry/Session.java +++ b/sentry/src/main/java/io/sentry/Session.java @@ -1,13 +1,13 @@ package io.sentry; import io.sentry.protocol.User; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.StringUtils; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Date; import java.util.Locale; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.jetbrains.annotations.ApiStatus; @@ -37,7 +37,7 @@ public enum State { private final @Nullable String distinctId; /** the SessionId, sid */ - private final @Nullable UUID sessionId; + private final @Nullable String sessionId; /** The session init flag */ private @Nullable Boolean init; @@ -67,7 +67,7 @@ public enum State { private @Nullable String abnormalMechanism; /** The session lock, ops should be atomic */ - private final @NotNull Object sessionLock = new Object(); + private final @NotNull AutoClosableReentrantLock sessionLock = new AutoClosableReentrantLock(); @SuppressWarnings("unused") private @Nullable Map unknown; @@ -78,7 +78,7 @@ public Session( final @Nullable Date timestamp, final int errorCount, final @Nullable String distinctId, - final @Nullable UUID sessionId, + final @Nullable String sessionId, final @Nullable Boolean init, final @Nullable Long sequence, final @Nullable Double duration, @@ -114,7 +114,7 @@ public Session( DateUtils.getCurrentDateTime(), 0, distinctId, - UUID.randomUUID(), + SentryUUID.generateSentryId(), true, null, null, @@ -141,7 +141,7 @@ public boolean isTerminated() { return distinctId; } - public @Nullable UUID getSessionId() { + public @Nullable String getSessionId() { return sessionId; } @@ -208,7 +208,7 @@ public void end() { * @param timestamp the timestamp or null */ public void end(final @Nullable Date timestamp) { - synchronized (sessionLock) { + try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { init = null; // at this state it might be Crashed already, so we don't check for it. @@ -262,7 +262,7 @@ public boolean update( final @Nullable String userAgent, final boolean addErrorsCount, final @Nullable String abnormalMechanism) { - synchronized (sessionLock) { + try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { boolean sessionHasBeenUpdated = false; if (status != null) { this.status = status; @@ -365,7 +365,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger throws IOException { writer.beginObject(); if (sessionId != null) { - writer.name(JsonKeys.SID).value(sessionId.toString()); + writer.name(JsonKeys.SID).value(sessionId); } if (distinctId != null) { writer.name(JsonKeys.DID).value(distinctId); @@ -434,7 +434,7 @@ public static final class Deserializer implements JsonDeserializer { Date timestamp = null; Integer errorCount = null; // @NotNull String distinctId = null; - UUID sessionId = null; + String sessionId = null; Boolean init = null; State status = null; // @NotNull Long sequence = null; @@ -450,12 +450,11 @@ public static final class Deserializer implements JsonDeserializer { final String nextName = reader.nextName(); switch (nextName) { case JsonKeys.SID: - String sidString = null; - try { - sidString = reader.nextStringOrNull(); - sessionId = UUID.fromString(sidString); - } catch (IllegalArgumentException e) { - logger.log(SentryLevel.ERROR, "%s sid is not valid.", sidString); + String sid = reader.nextStringOrNull(); + if (sid != null && (sid.length() == 36 || sid.length() == 32)) { + sessionId = sid; + } else { + logger.log(SentryLevel.ERROR, "%s sid is not valid.", sid); } break; case JsonKeys.DID: diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index d08cbc6ee4d..3d5bccc3d7b 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -10,7 +10,7 @@ import org.jetbrains.annotations.TestOnly; import org.jetbrains.annotations.VisibleForTesting; -/** Registers hook that flushes {@link Hub} when main thread shuts down. */ +/** Registers hook that flushes {@link Scopes} when main thread shuts down. */ public final class ShutdownHookIntegration implements Integration, Closeable { private final @NotNull Runtime runtime; @@ -27,12 +27,12 @@ public ShutdownHookIntegration() { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); + public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + Objects.requireNonNull(scopes, "Scopes are required"); Objects.requireNonNull(options, "SentryOptions is required"); if (options.isEnableShutdownHook()) { - thread = new Thread(() -> hub.flush(options.getFlushTimeoutMillis())); + thread = new Thread(() -> scopes.flush(options.getFlushTimeoutMillis())); handleShutdownInProgress( () -> { runtime.addShutdownHook(thread); diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index 14cf4825bff..3f08cca2a58 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -1,9 +1,8 @@ package io.sentry; -import io.sentry.metrics.LocalMetricsAggregator; +import io.sentry.protocol.Contexts; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; -import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; import java.util.ArrayList; import java.util.Iterator; @@ -35,7 +34,7 @@ public final class Span implements ISpan { /** A throwable thrown during the execution of the span. */ private @Nullable Throwable throwable; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private boolean finished = false; @@ -48,56 +47,43 @@ public final class Span implements ISpan { private final @NotNull Map data = new ConcurrentHashMap<>(); private final @NotNull Map measurements = new ConcurrentHashMap<>(); - @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references - private final @NotNull LazyEvaluator metricsAggregator = - new LazyEvaluator<>(() -> new LocalMetricsAggregator()); + private final @NotNull Contexts contexts = new Contexts(); Span( - final @NotNull SentryId traceId, - final @Nullable SpanId parentSpanId, final @NotNull SentryTracer transaction, - final @NotNull String operation, - final @NotNull IHub hub) { - this(traceId, parentSpanId, transaction, operation, hub, null, new SpanOptions(), null); - } - - Span( - final @NotNull SentryId traceId, - final @Nullable SpanId parentSpanId, - final @NotNull SentryTracer transaction, - final @NotNull String operation, - final @NotNull IHub hub, - final @Nullable SentryDate startTimestamp, + final @NotNull IScopes scopes, + final @NotNull SpanContext spanContext, final @NotNull SpanOptions options, final @Nullable SpanFinishedCallback spanFinishedCallback) { - this.context = - new SpanContext( - traceId, new SpanId(), operation, parentSpanId, transaction.getSamplingDecision()); + this.context = spanContext; + this.context.setOrigin(options.getOrigin()); this.transaction = Objects.requireNonNull(transaction, "transaction is required"); - this.hub = Objects.requireNonNull(hub, "hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = options; this.spanFinishedCallback = spanFinishedCallback; + final @Nullable SentryDate startTimestamp = options.getStartTimestamp(); if (startTimestamp != null) { this.startTimestamp = startTimestamp; } else { - this.startTimestamp = hub.getOptions().getDateProvider().now(); + this.startTimestamp = scopes.getOptions().getDateProvider().now(); } } public Span( final @NotNull TransactionContext context, final @NotNull SentryTracer sentryTracer, - final @NotNull IHub hub, - final @Nullable SentryDate startTimestamp, + final @NotNull IScopes scopes, final @NotNull SpanOptions options) { this.context = Objects.requireNonNull(context, "context is required"); + this.context.setOrigin(options.getOrigin()); this.transaction = Objects.requireNonNull(sentryTracer, "sentryTracer is required"); - this.hub = Objects.requireNonNull(hub, "hub is required"); + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); this.spanFinishedCallback = null; + final @Nullable SentryDate startTimestamp = options.getStartTimestamp(); if (startTimestamp != null) { this.startTimestamp = startTimestamp; } else { - this.startTimestamp = hub.getOptions().getDateProvider().now(); + this.startTimestamp = scopes.getOptions().getDateProvider().now(); } this.options = options; } @@ -151,6 +137,12 @@ public Span( return transaction.startChild(context.getSpanId(), operation, description, spanOptions); } + @Override + public @NotNull ISpan startChild( + @NotNull SpanContext spanContext, @NotNull SpanOptions spanOptions) { + return transaction.startChild(spanContext, spanOptions); + } + @Override public @NotNull ISpan startChild( @NotNull String operation, @@ -182,7 +174,7 @@ public void finish() { @Override public void finish(@Nullable SpanStatus status) { - finish(status, hub.getOptions().getDateProvider().now()); + finish(status, scopes.getOptions().getDateProvider().now()); } /** @@ -199,7 +191,7 @@ public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate } this.context.setStatus(status); - this.timestamp = timestamp == null ? hub.getOptions().getDateProvider().now() : timestamp; + this.timestamp = timestamp == null ? scopes.getOptions().getDateProvider().now() : timestamp; if (options.isTrimStart() || options.isTrimEnd()) { @Nullable SentryDate minChildStart = null; @Nullable SentryDate maxChildEnd = null; @@ -232,7 +224,7 @@ public void finish(final @Nullable SpanStatus status, final @Nullable SentryDate } if (throwable != null) { - hub.setSpanContext(throwable, this, this.transaction.getName()); + scopes.setSpanContext(throwable, this, this.transaction.getName()); } if (spanFinishedCallback != null) { spanFinishedCallback.execute(this); @@ -294,6 +286,7 @@ public boolean isFinished() { return data; } + @Override public @Nullable Boolean isSampled() { return context.getSampled(); } @@ -302,6 +295,7 @@ public boolean isFinished() { return context.getProfileSampled(); } + @Override public @Nullable TracesSamplingDecision getSamplingDecision() { return context.getSamplingDecision(); } @@ -346,7 +340,8 @@ public void setData(final @NotNull String key, final @NotNull Object value) { @Override public void setMeasurement(final @NotNull String name, final @NotNull Number value) { if (isFinished()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -368,7 +363,8 @@ public void setMeasurement( final @NotNull Number value, final @NotNull MeasurementUnit unit) { if (isFinished()) { - hub.getOptions() + scopes + .getOptions() .getLogger() .log( SentryLevel.DEBUG, @@ -404,8 +400,13 @@ public boolean isNoOp() { } @Override - public @NotNull LocalMetricsAggregator getLocalMetricsAggregator() { - return metricsAggregator.getValue(); + public void setContext(@NotNull String key, @NotNull Object context) { + this.contexts.put(key, context); + } + + @Override + public @NotNull Contexts getContexts() { + return contexts; } void setSpanFinishedCallback(final @Nullable SpanFinishedCallback callback) { @@ -439,4 +440,9 @@ private List getDirectChildren() { } return children; } + + @Override + public @NotNull ISentryLifecycleToken makeCurrent() { + return NoOpScopesLifecycleToken.getInstance(); + } } diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 5a43ff845e0..91e3abd9560 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -16,6 +16,7 @@ @Open public class SpanContext implements JsonUnknown, JsonSerializable { public static final String TYPE = "trace"; + public static final String DEFAULT_ORIGIN = "manual"; /** Determines which trace the Span belongs to. */ private final @NotNull SentryId traceId; @@ -24,7 +25,7 @@ public class SpanContext implements JsonUnknown, JsonSerializable { private final @NotNull SpanId spanId; /** Id of a parent span. */ - private final @Nullable SpanId parentSpanId; + private @Nullable SpanId parentSpanId; private transient @Nullable TracesSamplingDecision samplingDecision; @@ -43,11 +44,16 @@ public class SpanContext implements JsonUnknown, JsonSerializable { /** A map or list of tags for this event. Each tag must be less than 200 characters. */ protected @NotNull Map tags = new ConcurrentHashMap<>(); - /** Describes the status of the Transaction. */ - protected @Nullable String origin = "manual"; + protected @Nullable String origin = DEFAULT_ORIGIN; + + protected @NotNull Map data = new ConcurrentHashMap<>(); private @Nullable Map unknown; + private @NotNull Instrumenter instrumenter = Instrumenter.SENTRY; + + protected @Nullable Baggage baggage; + public SpanContext( final @NotNull String operation, final @Nullable TracesSamplingDecision samplingDecision) { this(new SentryId(), new SpanId(), operation, null, samplingDecision); @@ -68,7 +74,7 @@ public SpanContext( final @NotNull String operation, final @Nullable SpanId parentSpanId, final @Nullable TracesSamplingDecision samplingDecision) { - this(traceId, spanId, parentSpanId, operation, null, samplingDecision, null, "manual"); + this(traceId, spanId, parentSpanId, operation, null, samplingDecision, null, DEFAULT_ORIGIN); } @ApiStatus.Internal @@ -213,6 +219,42 @@ public void setOrigin(final @Nullable String origin) { this.origin = origin; } + public @NotNull Instrumenter getInstrumenter() { + return instrumenter; + } + + public void setInstrumenter(final @NotNull Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + public @Nullable Baggage getBaggage() { + return baggage; + } + + public @NotNull Map getData() { + return data; + } + + public void setData(final @NotNull String key, final @NotNull Object value) { + data.put(key, value); + } + + @ApiStatus.Internal + public SpanContext copyForChild( + final @NotNull String operation, + final @Nullable SpanId parentSpanId, + final @Nullable SpanId spanId) { + return new SpanContext( + traceId, + spanId == null ? new SpanId() : spanId, + parentSpanId, + operation, + null, + samplingDecision, + null, + DEFAULT_ORIGIN); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -223,12 +265,12 @@ public boolean equals(Object o) { && Objects.equals(parentSpanId, that.parentSpanId) && op.equals(that.op) && Objects.equals(description, that.description) - && status == that.status; + && getStatus() == that.getStatus(); } @Override public int hashCode() { - return Objects.hash(traceId, spanId, parentSpanId, op, description, status); + return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus()); } // region JsonSerializable @@ -242,6 +284,7 @@ public static final class JsonKeys { public static final String STATUS = "status"; public static final String TAGS = "tags"; public static final String ORIGIN = "origin"; + public static final String DATA = "data"; } @Override @@ -260,8 +303,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (description != null) { writer.name(JsonKeys.DESCRIPTION).value(description); } - if (status != null) { - writer.name(JsonKeys.STATUS).value(logger, status); + if (getStatus() != null) { + writer.name(JsonKeys.STATUS).value(logger, getStatus()); } if (origin != null) { writer.name(JsonKeys.ORIGIN).value(logger, origin); @@ -269,6 +312,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (!tags.isEmpty()) { writer.name(JsonKeys.TAGS).value(logger, tags); } + if (!data.isEmpty()) { + writer.name(JsonKeys.DATA).value(logger, data); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -303,6 +349,7 @@ public static final class Deserializer implements JsonDeserializer SpanStatus status = null; String origin = null; Map tags = null; + Map data = null; Map unknown = null; while (reader.peek() == JsonToken.NAME) { @@ -334,6 +381,9 @@ public static final class Deserializer implements JsonDeserializer CollectionUtils.newConcurrentHashMap( (Map) reader.nextObjectOrNull()); break; + case JsonKeys.DATA: + data = (Map) reader.nextObjectOrNull(); + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); @@ -371,9 +421,15 @@ public static final class Deserializer implements JsonDeserializer spanContext.setDescription(description); spanContext.setStatus(status); spanContext.setOrigin(origin); + if (tags != null) { spanContext.tags = tags; } + + if (data != null) { + spanContext.data = data; + } + spanContext.setUnknown(unknown); reader.endObject(); return spanContext; diff --git a/sentry/src/main/java/io/sentry/SpanFactoryFactory.java b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java new file mode 100644 index 00000000000..7dbb9f1f588 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SpanFactoryFactory.java @@ -0,0 +1,42 @@ +package io.sentry; + +import io.sentry.util.LoadClass; +import io.sentry.util.Platform; +import java.lang.reflect.InvocationTargetException; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SpanFactoryFactory { + + private static final String OTEL_SPAN_FACTORY = "io.sentry.opentelemetry.OtelSpanFactory"; + + public static @NotNull ISpanFactory create( + final @NotNull LoadClass loadClass, final @NotNull ILogger logger) { + if (Platform.isJvm()) { + if (loadClass.isClassAvailable(OTEL_SPAN_FACTORY, logger)) { + Class otelSpanFactoryClazz = loadClass.loadClass(OTEL_SPAN_FACTORY, logger); + if (otelSpanFactoryClazz != null) { + try { + final @Nullable Object otelSpanFactory = + otelSpanFactoryClazz.getDeclaredConstructor().newInstance(); + if (otelSpanFactory != null && otelSpanFactory instanceof ISpanFactory) { + return (ISpanFactory) otelSpanFactory; + } + } catch (InstantiationException e) { + // TODO log + } catch (IllegalAccessException e) { + // TODO log + } catch (InvocationTargetException e) { + // TODO log + } catch (NoSuchMethodException e) { + // TODO log + } + } + } + } + + return new DefaultSpanFactory(); + } +} diff --git a/sentry/src/main/java/io/sentry/SpanFinishedCallback.java b/sentry/src/main/java/io/sentry/SpanFinishedCallback.java index 9ce34dc7640..55f5a66f0b0 100644 --- a/sentry/src/main/java/io/sentry/SpanFinishedCallback.java +++ b/sentry/src/main/java/io/sentry/SpanFinishedCallback.java @@ -1,8 +1,10 @@ package io.sentry; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -interface SpanFinishedCallback { +@ApiStatus.Internal +public interface SpanFinishedCallback { /** * Called when observed span finishes. * diff --git a/sentry/src/main/java/io/sentry/SpanId.java b/sentry/src/main/java/io/sentry/SpanId.java index 70608fb7cbb..fcc7f3a4f38 100644 --- a/sentry/src/main/java/io/sentry/SpanId.java +++ b/sentry/src/main/java/io/sentry/SpanId.java @@ -1,26 +1,25 @@ package io.sentry; -import io.sentry.util.Objects; -import io.sentry.util.StringUtils; +import static io.sentry.util.StringUtils.PROPER_NIL_UUID; + +import io.sentry.util.LazyEvaluator; import java.io.IOException; -import java.util.UUID; +import java.util.Objects; import org.jetbrains.annotations.NotNull; public final class SpanId implements JsonSerializable { - public static final SpanId EMPTY_ID = new SpanId(new UUID(0, 0)); + public static final SpanId EMPTY_ID = + new SpanId(PROPER_NIL_UUID.replace("-", "").substring(0, 16)); - private final @NotNull String value; + private final @NotNull LazyEvaluator lazyValue; public SpanId(final @NotNull String value) { - this.value = Objects.requireNonNull(value, "value is required"); + Objects.requireNonNull(value, "value is required"); + this.lazyValue = new LazyEvaluator<>(() -> value); } public SpanId() { - this(UUID.randomUUID()); - } - - private SpanId(final @NotNull UUID uuid) { - this(StringUtils.normalizeUUID(uuid.toString()).replace("-", "").substring(0, 16)); + this.lazyValue = new LazyEvaluator<>(SentryUUID::generateSpanId); } @Override @@ -28,17 +27,17 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SpanId spanId = (SpanId) o; - return value.equals(spanId.value); + return lazyValue.getValue().equals(spanId.lazyValue.getValue()); } @Override public int hashCode() { - return value.hashCode(); + return lazyValue.getValue().hashCode(); } @Override public String toString() { - return this.value; + return lazyValue.getValue(); } // JsonElementSerializer @@ -46,7 +45,7 @@ public String toString() { @Override public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) throws IOException { - writer.value(value); + writer.value(lazyValue.getValue()); } // JsonElementDeserializer diff --git a/sentry/src/main/java/io/sentry/SpanOptions.java b/sentry/src/main/java/io/sentry/SpanOptions.java index 42fc9906a34..3407a43bac8 100644 --- a/sentry/src/main/java/io/sentry/SpanOptions.java +++ b/sentry/src/main/java/io/sentry/SpanOptions.java @@ -1,12 +1,37 @@ package io.sentry; +import static io.sentry.SpanContext.DEFAULT_ORIGIN; + import com.jakewharton.nopen.annotation.Open; -import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -@ApiStatus.Internal @Open public class SpanOptions { + /** The start timestamp of the transaction */ + private @Nullable SentryDate startTimestamp = null; + + private @NotNull ScopeBindingMode scopeBindingMode = ScopeBindingMode.AUTO; + + /** + * Gets the startTimestamp + * + * @return startTimestamp - the startTimestamp + */ + public @Nullable SentryDate getStartTimestamp() { + return startTimestamp; + } + + /** + * Sets the startTimestamp + * + * @param startTimestamp - the startTimestamp + */ + public void setStartTimestamp(@Nullable SentryDate startTimestamp) { + this.startTimestamp = startTimestamp; + } + /** * If `trimStart` is true, sets the start timestamp of the transaction to the lowest start * timestamp of child spans. @@ -27,6 +52,8 @@ public class SpanOptions { */ private boolean isIdle = false; + protected @Nullable String origin = DEFAULT_ORIGIN; + public boolean isTrimStart() { return trimStart; } @@ -50,4 +77,20 @@ public void setTrimEnd(boolean trimEnd) { public void setIdle(boolean idle) { isIdle = idle; } + + public @Nullable String getOrigin() { + return origin; + } + + public void setOrigin(final @Nullable String origin) { + this.origin = origin; + } + + public @NotNull ScopeBindingMode getScopeBindingMode() { + return scopeBindingMode; + } + + public void setScopeBindingMode(final @NotNull ScopeBindingMode scopeBindingMode) { + this.scopeBindingMode = scopeBindingMode; + } } diff --git a/sentry/src/main/java/io/sentry/SpanStatus.java b/sentry/src/main/java/io/sentry/SpanStatus.java index 5185d27e058..3cb98e3bffe 100644 --- a/sentry/src/main/java/io/sentry/SpanStatus.java +++ b/sentry/src/main/java/io/sentry/SpanStatus.java @@ -7,7 +7,7 @@ public enum SpanStatus implements JsonSerializable { /** Not an error, returned on success. */ - OK(200, 299), + OK(0, 399), /** The operation was cancelled, typically by the caller. */ CANCELLED(499), /** @@ -103,12 +103,27 @@ private boolean matches(int httpStatusCode) { return httpStatusCode >= minHttpStatusCode && httpStatusCode <= maxHttpStatusCode; } + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } + + public static @Nullable SpanStatus fromApiNameSafely(final @Nullable String apiName) { + if (apiName == null) { + return null; + } + try { + return SpanStatus.valueOf(apiName.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + return null; + } + } + // JsonSerializable @Override public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) throws IOException { - writer.value(name().toLowerCase(Locale.ROOT)); + writer.value(apiName()); } public static final class Deserializer implements JsonDeserializer { diff --git a/sentry/src/main/java/io/sentry/SpotlightIntegration.java b/sentry/src/main/java/io/sentry/SpotlightIntegration.java index 6d488bcbce9..0b69ae79be7 100644 --- a/sentry/src/main/java/io/sentry/SpotlightIntegration.java +++ b/sentry/src/main/java/io/sentry/SpotlightIntegration.java @@ -26,7 +26,7 @@ public final class SpotlightIntegration private @NotNull ISentryExecutorService executorService = NoOpSentryExecutorService.getInstance(); @Override - public void register(@NotNull IHub hub, @NotNull SentryOptions options) { + public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { this.options = options; this.logger = options.getLogger(); diff --git a/sentry/src/main/java/io/sentry/Stack.java b/sentry/src/main/java/io/sentry/Stack.java index adb43f82124..9b768ad2746 100644 --- a/sentry/src/main/java/io/sentry/Stack.java +++ b/sentry/src/main/java/io/sentry/Stack.java @@ -1,11 +1,13 @@ package io.sentry; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import java.util.Deque; import java.util.Iterator; import java.util.concurrent.LinkedBlockingDeque; import org.jetbrains.annotations.NotNull; +/** TODO [POTEL] can this class be removed? */ final class Stack { static final class StackItem { @@ -47,6 +49,7 @@ public void setClient(final @NotNull ISentryClient client) { private final @NotNull Deque items = new LinkedBlockingDeque<>(); private final @NotNull ILogger logger; + private final @NotNull AutoClosableReentrantLock itemsLock = new AutoClosableReentrantLock(); public Stack(final @NotNull ILogger logger, final @NotNull StackItem rootStackItem) { this.logger = Objects.requireNonNull(logger, "logger is required"); @@ -73,7 +76,7 @@ StackItem peek() { } void pop() { - synchronized (items) { + try (final @NotNull ISentryLifecycleToken ignored = itemsLock.acquire()) { if (items.size() != 1) { items.pop(); } else { diff --git a/sentry/src/main/java/io/sentry/SynchronizedCollection.java b/sentry/src/main/java/io/sentry/SynchronizedCollection.java index 8693b45eca2..e7eccebf63a 100644 --- a/sentry/src/main/java/io/sentry/SynchronizedCollection.java +++ b/sentry/src/main/java/io/sentry/SynchronizedCollection.java @@ -19,9 +19,11 @@ package io.sentry; import com.jakewharton.nopen.annotation.Open; +import io.sentry.util.AutoClosableReentrantLock; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; +import org.jetbrains.annotations.NotNull; /** * Decorates another {@link Collection} to synchronize its behaviour for a multi-threaded @@ -50,7 +52,7 @@ class SynchronizedCollection implements Collection, Serializable { /** The collection to decorate */ private final Collection collection; /** The object to lock on, needed for List/SortedSet views */ - final Object lock; + final AutoClosableReentrantLock lock; /** * Factory method to create a synchronized collection. @@ -77,7 +79,7 @@ public static SynchronizedCollection synchronizedCollection(final Collect throw new NullPointerException("Collection must not be null."); } this.collection = collection; - this.lock = this; + this.lock = new AutoClosableReentrantLock(); } /** @@ -87,7 +89,7 @@ public static SynchronizedCollection synchronizedCollection(final Collect * @param lock the lock object to use, must not be null * @throws NullPointerException if the collection or lock is null */ - SynchronizedCollection(final Collection collection, final Object lock) { + SynchronizedCollection(final Collection collection, final AutoClosableReentrantLock lock) { if (collection == null) { throw new NullPointerException("Collection must not be null."); } @@ -111,42 +113,42 @@ protected Collection decorated() { @Override public boolean add(final E object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().add(object); } } @Override public boolean addAll(final Collection coll) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().addAll(coll); } } @Override public void clear() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { decorated().clear(); } } @Override public boolean contains(final Object object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().contains(object); } } @Override public boolean containsAll(final Collection coll) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().containsAll(coll); } } @Override public boolean isEmpty() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().isEmpty(); } } @@ -170,42 +172,42 @@ public Iterator iterator() { @Override public Object[] toArray() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().toArray(); } } @Override public T[] toArray(final T[] object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().toArray(object); } } @Override public boolean remove(final Object object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().remove(object); } } @Override public boolean removeAll(final Collection coll) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().removeAll(coll); } } @Override public boolean retainAll(final Collection coll) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().retainAll(coll); } } @Override public int size() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().size(); } } @@ -213,7 +215,7 @@ public int size() { @SuppressWarnings("UndefinedEquals") @Override public boolean equals(final Object object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (object == this) { return true; } @@ -223,14 +225,14 @@ public boolean equals(final Object object) { @Override public int hashCode() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().hashCode(); } } @Override public String toString() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().toString(); } } diff --git a/sentry/src/main/java/io/sentry/SynchronizedQueue.java b/sentry/src/main/java/io/sentry/SynchronizedQueue.java index 68b75c19534..8e9964a203b 100644 --- a/sentry/src/main/java/io/sentry/SynchronizedQueue.java +++ b/sentry/src/main/java/io/sentry/SynchronizedQueue.java @@ -18,7 +18,9 @@ */ package io.sentry; +import io.sentry.util.AutoClosableReentrantLock; import java.util.Queue; +import org.jetbrains.annotations.NotNull; /** * Decorates another {@link Queue} to synchronize its behaviour for a multi-threaded environment. @@ -65,7 +67,7 @@ private SynchronizedQueue(final Queue queue) { * @throws NullPointerException if queue or lock is null */ @SuppressWarnings("ProtectedMembersInFinalClass") - protected SynchronizedQueue(final Queue queue, final Object lock) { + protected SynchronizedQueue(final Queue queue, final AutoClosableReentrantLock lock) { super(queue, lock); } @@ -81,7 +83,7 @@ protected Queue decorated() { @Override public E element() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().element(); } } @@ -92,7 +94,7 @@ public boolean equals(final Object object) { if (object == this) { return true; } - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().equals(object); } } @@ -101,49 +103,49 @@ public boolean equals(final Object object) { @Override public int hashCode() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().hashCode(); } } @Override public boolean offer(final E e) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().offer(e); } } @Override public E peek() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().peek(); } } @Override public E poll() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().poll(); } } @Override public E remove() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().remove(); } } @Override public Object[] toArray() { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().toArray(); } } @Override public T[] toArray(T[] object) { - synchronized (lock) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { return decorated().toArray(object); } } diff --git a/sentry/src/main/java/io/sentry/TraceContext.java b/sentry/src/main/java/io/sentry/TraceContext.java index f3d603b7c01..bb32022f605 100644 --- a/sentry/src/main/java/io/sentry/TraceContext.java +++ b/sentry/src/main/java/io/sentry/TraceContext.java @@ -17,7 +17,6 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { private final @Nullable String release; private final @Nullable String environment; private final @Nullable String userId; - private final @Nullable String userSegment; private final @Nullable String transaction; private final @Nullable String sampleRate; private final @Nullable String sampled; @@ -40,40 +39,11 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { @Nullable String sampleRate, @Nullable String sampled, @Nullable SentryId replayId) { - this( - traceId, - publicKey, - release, - environment, - userId, - null, - transaction, - sampleRate, - sampled, - replayId); - } - - /** - * @deprecated segment has no effect and will be removed in the next major update. - */ - @Deprecated - TraceContext( - @NotNull SentryId traceId, - @NotNull String publicKey, - @Nullable String release, - @Nullable String environment, - @Nullable String userId, - @Nullable String userSegment, - @Nullable String transaction, - @Nullable String sampleRate, - @Nullable String sampled, - @Nullable SentryId replayId) { this.traceId = traceId; this.publicKey = publicKey; this.release = release; this.environment = environment; this.userId = userId; - this.userSegment = userSegment; this.transaction = transaction; this.sampleRate = sampleRate; this.sampled = sampled; @@ -110,14 +80,6 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { return userId; } - /** - * @deprecated has no effect and will be removed in the next major update. - */ - @Deprecated - public @Nullable String getUserSegment() { - return userSegment; - } - public @Nullable String getTransaction() { return transaction; } @@ -134,88 +96,6 @@ public final class TraceContext implements JsonUnknown, JsonSerializable { return replayId; } - /** - * @deprecated only here to support parsing legacy JSON with non flattened user - */ - @Deprecated - private static final class TraceContextUser implements JsonUnknown { - private @Nullable String id; - private @Nullable String segment; - - @SuppressWarnings("unused") - private @Nullable Map unknown; - - private TraceContextUser(final @Nullable String id, final @Nullable String segment) { - this.id = id; - this.segment = segment; - } - - public @Nullable String getId() { - return id; - } - - /** - * @deprecated has no effect and will be removed in the next major update. - */ - @Deprecated - public @Nullable String getSegment() { - return segment; - } - - // region json - - @Nullable - @Override - public Map getUnknown() { - return unknown; - } - - @Override - public void setUnknown(@Nullable Map unknown) { - this.unknown = unknown; - } - - public static final class JsonKeys { - public static final String ID = "id"; - public static final String SEGMENT = "segment"; - } - - public static final class Deserializer implements JsonDeserializer { - @Override - public @NotNull TraceContextUser deserialize( - @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { - reader.beginObject(); - - String id = null; - String segment = null; - Map unknown = null; - while (reader.peek() == JsonToken.NAME) { - final String nextName = reader.nextName(); - switch (nextName) { - case TraceContextUser.JsonKeys.ID: - id = reader.nextStringOrNull(); - break; - case TraceContextUser.JsonKeys.SEGMENT: - segment = reader.nextStringOrNull(); - break; - default: - if (unknown == null) { - unknown = new ConcurrentHashMap<>(); - } - reader.nextUnknown(logger, unknown, nextName); - break; - } - } - TraceContextUser traceStateUser = new TraceContextUser(id, segment); - traceStateUser.setUnknown(unknown); - reader.endObject(); - return traceStateUser; - } - } - - // endregion - } - // region json @Nullable @@ -234,9 +114,7 @@ public static final class JsonKeys { public static final String PUBLIC_KEY = "public_key"; public static final String RELEASE = "release"; public static final String ENVIRONMENT = "environment"; - public static final String USER = "user"; public static final String USER_ID = "user_id"; - public static final String USER_SEGMENT = "user_segment"; public static final String TRANSACTION = "transaction"; public static final String SAMPLE_RATE = "sample_rate"; public static final String SAMPLED = "sampled"; @@ -258,9 +136,6 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (userId != null) { writer.name(TraceContext.JsonKeys.USER_ID).value(userId); } - if (userSegment != null) { - writer.name(TraceContext.JsonKeys.USER_SEGMENT).value(userSegment); - } if (transaction != null) { writer.name(TraceContext.JsonKeys.TRANSACTION).value(transaction); } @@ -293,9 +168,7 @@ public static final class Deserializer implements JsonDeserializer String publicKey = null; String release = null; String environment = null; - TraceContextUser user = null; String userId = null; - String userSegment = null; String transaction = null; String sampleRate = null; String sampled = null; @@ -317,15 +190,9 @@ public static final class Deserializer implements JsonDeserializer case TraceContext.JsonKeys.ENVIRONMENT: environment = reader.nextStringOrNull(); break; - case TraceContext.JsonKeys.USER: - user = reader.nextOrNull(logger, new TraceContextUser.Deserializer()); - break; case TraceContext.JsonKeys.USER_ID: userId = reader.nextStringOrNull(); break; - case TraceContext.JsonKeys.USER_SEGMENT: - userSegment = reader.nextStringOrNull(); - break; case TraceContext.JsonKeys.TRANSACTION: transaction = reader.nextStringOrNull(); break; @@ -352,14 +219,6 @@ public static final class Deserializer implements JsonDeserializer if (publicKey == null) { throw missingRequiredFieldException(TraceContext.JsonKeys.PUBLIC_KEY, logger); } - if (user != null) { - if (userId == null) { - userId = user.getId(); - } - if (userSegment == null) { - userSegment = user.getSegment(); - } - } TraceContext traceContext = new TraceContext( traceId, @@ -367,7 +226,6 @@ public static final class Deserializer implements JsonDeserializer release, environment, userId, - userSegment, transaction, sampleRate, sampled, diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index ef04cae3695..3ce28ab7451 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -3,13 +3,13 @@ import io.sentry.util.Objects; import io.sentry.util.Random; import io.sentry.util.SentryRandom; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -final class TracesSampler { - private static final @NotNull Double DEFAULT_TRACES_SAMPLE_RATE = 1.0; - +@ApiStatus.Internal +public final class TracesSampler { private final @NotNull SentryOptions options; private final @Nullable Random random; @@ -25,7 +25,7 @@ public TracesSampler(final @NotNull SentryOptions options) { @SuppressWarnings("deprecation") @NotNull - TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { + public TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { final TracesSamplingDecision samplingContextSamplingDecision = samplingContext.getTransactionContext().getSamplingDecision(); if (samplingContextSamplingDecision != null) { @@ -69,15 +69,10 @@ TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { } final @Nullable Double tracesSampleRateFromOptions = options.getTracesSampleRate(); - final @Nullable Boolean isEnableTracing = options.getEnableTracing(); - final @Nullable Double defaultSampleRate = - Boolean.TRUE.equals(isEnableTracing) ? DEFAULT_TRACES_SAMPLE_RATE : null; - final @Nullable Double tracesSampleRateOrDefault = - tracesSampleRateFromOptions == null ? defaultSampleRate : tracesSampleRateFromOptions; final @NotNull Double downsampleFactor = Math.pow(2, options.getBackpressureMonitor().getDownsampleFactor()); final @Nullable Double downsampledTracesSampleRate = - tracesSampleRateOrDefault == null ? null : tracesSampleRateOrDefault / downsampleFactor; + tracesSampleRateFromOptions == null ? null : tracesSampleRateFromOptions / downsampleFactor; if (downsampledTracesSampleRate != null) { return new TracesSamplingDecision( diff --git a/sentry/src/main/java/io/sentry/TransactionContext.java b/sentry/src/main/java/io/sentry/TransactionContext.java index 9dfebd34535..aec0b8927de 100644 --- a/sentry/src/main/java/io/sentry/TransactionContext.java +++ b/sentry/src/main/java/io/sentry/TransactionContext.java @@ -3,57 +3,20 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; import io.sentry.util.Objects; -import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class TransactionContext extends SpanContext { - private static final @NotNull String DEFAULT_NAME = ""; + public static final @NotNull String DEFAULT_TRANSACTION_NAME = ""; private static final @NotNull TransactionNameSource DEFAULT_NAME_SOURCE = TransactionNameSource.CUSTOM; private static final @NotNull String DEFAULT_OPERATION = "default"; private @NotNull String name; private @NotNull TransactionNameSource transactionNameSource; private @Nullable TracesSamplingDecision parentSamplingDecision; - private @Nullable Baggage baggage; - private @NotNull Instrumenter instrumenter = Instrumenter.SENTRY; private boolean isForNextAppStart = false; - /** - * Creates {@link TransactionContext} from sentry-trace header. - * - * @param name - the transaction name - * @param operation - the operation - * @param sentryTrace - the sentry-trace header - * @deprecated use {@link Sentry#continueTrace(String, List)} and setters for name and operation - * here instead. - * @return the transaction contexts - */ - @Deprecated - public static @NotNull TransactionContext fromSentryTrace( - final @NotNull String name, - final @NotNull String operation, - final @NotNull SentryTraceHeader sentryTrace) { - @Nullable Boolean parentSampled = sentryTrace.isSampled(); - TracesSamplingDecision samplingDecision = - parentSampled == null ? null : new TracesSamplingDecision(parentSampled); - - TransactionContext transactionContext = - new TransactionContext( - sentryTrace.getTraceId(), - new SpanId(), - sentryTrace.getSpanId(), - samplingDecision, - null); - - transactionContext.setName(name); - transactionContext.setTransactionNameSource(TransactionNameSource.CUSTOM); - transactionContext.setOperation(operation); - - return transactionContext; - } - @ApiStatus.Internal public static TransactionContext fromPropagationContext( final @NotNull PropagationContext propagationContext) { @@ -67,11 +30,12 @@ public static TransactionContext fromPropagationContext( baggage.freeze(); Double sampleRate = baggage.getSampleRateDouble(); - Boolean sampled = parentSampled != null ? parentSampled.booleanValue() : false; - if (sampleRate != null) { - samplingDecision = new TracesSamplingDecision(sampled, sampleRate); - } else { - samplingDecision = new TracesSamplingDecision(sampled); + if (parentSampled != null) { + if (sampleRate != null) { + samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue(), sampleRate); + } else { + samplingDecision = new TracesSamplingDecision(parentSampled.booleanValue()); + } } } @@ -136,7 +100,7 @@ public TransactionContext( final @Nullable TracesSamplingDecision parentSamplingDecision, final @Nullable Baggage baggage) { super(traceId, spanId, DEFAULT_OPERATION, parentSpanId, null); - this.name = DEFAULT_NAME; + this.name = DEFAULT_TRANSACTION_NAME; this.parentSamplingDecision = parentSamplingDecision; this.transactionNameSource = DEFAULT_NAME_SOURCE; this.baggage = baggage; @@ -158,10 +122,6 @@ public TransactionContext( return parentSamplingDecision; } - public @Nullable Baggage getBaggage() { - return baggage; - } - public void setParentSampled(final @Nullable Boolean parentSampled) { if (parentSampled == null) { this.parentSamplingDecision = null; @@ -186,14 +146,6 @@ public void setParentSampled( return transactionNameSource; } - public @NotNull Instrumenter getInstrumenter() { - return instrumenter; - } - - public void setInstrumenter(final @NotNull Instrumenter instrumenter) { - this.instrumenter = instrumenter; - } - public void setName(final @NotNull String name) { this.name = Objects.requireNonNull(name, "name is required"); } diff --git a/sentry/src/main/java/io/sentry/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 6d0eac8b7b7..0da3cf611c2 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -1,6 +1,7 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Sentry Transaction options */ @@ -14,12 +15,6 @@ public final class TransactionOptions extends SpanOptions { */ private @Nullable CustomSamplingContext customSamplingContext = null; - /** Defines if transaction should be bound to scope */ - private boolean bindToScope = false; - - /** The start timestamp of the transaction */ - private @Nullable SentryDate startTimestamp = null; - /** Defines if transaction refers to the app start process */ private boolean isAppStartTransaction = false; @@ -56,6 +51,9 @@ public final class TransactionOptions extends SpanOptions { */ private @Nullable TransactionFinishedCallback transactionFinishedCallback = null; + /** Span factory to use. Uses factory configured in {@link SentryOptions} if `null`. */ + @ApiStatus.Internal private @Nullable ISpanFactory spanFactory = null; + /** * Gets the customSamplingContext * @@ -80,7 +78,7 @@ public void setCustomSamplingContext(@Nullable CustomSamplingContext customSampl * @return true if enabled or false otherwise */ public boolean isBindToScope() { - return bindToScope; + return ScopeBindingMode.ON == getScopeBindingMode(); } /** @@ -89,25 +87,7 @@ public boolean isBindToScope() { * @param bindToScope true if enabled or false otherwise */ public void setBindToScope(boolean bindToScope) { - this.bindToScope = bindToScope; - } - - /** - * Gets the startTimestamp - * - * @return startTimestamp - the startTimestamp - */ - public @Nullable SentryDate getStartTimestamp() { - return startTimestamp; - } - - /** - * Sets the startTimestamp - * - * @param startTimestamp - the startTimestamp - */ - public void setStartTimestamp(@Nullable SentryDate startTimestamp) { - this.startTimestamp = startTimestamp; + setScopeBindingMode(bindToScope ? ScopeBindingMode.ON : ScopeBindingMode.OFF); } /** @@ -196,4 +176,14 @@ public void setAppStartTransaction(final boolean appStartTransaction) { public boolean isAppStartTransaction() { return isAppStartTransaction; } + + @ApiStatus.Internal + public @Nullable ISpanFactory getSpanFactory() { + return this.spanFactory; + } + + @ApiStatus.Internal + public void setSpanFactory(final @NotNull ISpanFactory spanFactory) { + this.spanFactory = spanFactory; + } } diff --git a/sentry/src/main/java/io/sentry/TypeCheckHint.java b/sentry/src/main/java/io/sentry/TypeCheckHint.java index e9d219c3850..cbe784db5b8 100644 --- a/sentry/src/main/java/io/sentry/TypeCheckHint.java +++ b/sentry/src/main/java/io/sentry/TypeCheckHint.java @@ -55,6 +55,9 @@ public final class TypeCheckHint { /** Used for GraphQl handler exceptions. */ public static final String GRAPHQL_HANDLER_PARAMETERS = "graphql:handlerParameters"; + /** Used for GraphQl data fetcher breadcrumbs. */ + public static final String GRAPHQL_DATA_FETCHING_ENVIRONMENT = "graphql:dataFetchingEnvironment"; + /** Used for JUL breadcrumbs. */ public static final String JUL_LOG_RECORD = "jul:logRecord"; diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 1eddb762aab..1cf4c151b0c 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -28,7 +28,7 @@ public final class UncaughtExceptionHandlerIntegration /** Reference to the pre-existing uncaught exception handler. */ private @Nullable Thread.UncaughtExceptionHandler defaultExceptionHandler; - private @Nullable IHub hub; + private @Nullable IScopes scopes; private @Nullable SentryOptions options; private boolean registered = false; @@ -43,7 +43,7 @@ public UncaughtExceptionHandlerIntegration() { } @Override - public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + public final void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { if (registered) { options .getLogger() @@ -54,7 +54,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio } registered = true; - this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); this.options = Objects.requireNonNull(options, "SentryOptions is required"); this.options @@ -96,7 +96,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio @Override public void uncaughtException(Thread thread, Throwable thrown) { - if (options != null && hub != null) { + if (options != null && scopes != null) { options.getLogger().log(SentryLevel.INFO, "Uncaught exception received."); try { @@ -106,14 +106,14 @@ public void uncaughtException(Thread thread, Throwable thrown) { final SentryEvent event = new SentryEvent(throwable); event.setLevel(SentryLevel.FATAL); - final ITransaction transaction = hub.getTransaction(); + final ITransaction transaction = scopes.getTransaction(); if (transaction == null && event.getEventId() != null) { // if there's no active transaction on scope, this event can trigger flush notification exceptionHint.setFlushable(event.getEventId()); } final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint); - final @NotNull SentryId sentryId = hub.captureEvent(event, hint); + final @NotNull SentryId sentryId = scopes.captureEvent(event, hint); final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID); final EventDropReason eventDropReason = HintUtils.getEventDropReason(hint); // in case the event has been dropped by multithreaded deduplicator, the other threads will diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java index 2008a38c761..92fa3ce0a5c 100644 --- a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -1,10 +1,14 @@ package io.sentry.backpressure; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.AutoClosableReentrantLock; +import java.util.concurrent.Future; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class BackpressureMonitor implements IBackpressureMonitor, Runnable { static final int MAX_DOWNSAMPLE_FACTOR = 10; @@ -12,12 +16,15 @@ public final class BackpressureMonitor implements IBackpressureMonitor, Runnable private static final int CHECK_INTERVAL_IN_MS = 10 * 1000; private final @NotNull SentryOptions sentryOptions; - private final @NotNull IHub hub; + private final @NotNull IScopes scopes; private int downsampleFactor = 0; + private volatile @Nullable Future latestScheduledRun = null; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); - public BackpressureMonitor(final @NotNull SentryOptions sentryOptions, final @NotNull IHub hub) { + public BackpressureMonitor( + final @NotNull SentryOptions sentryOptions, final @NotNull IScopes scopes) { this.sentryOptions = sentryOptions; - this.hub = hub; + this.scopes = scopes; } @Override @@ -36,6 +43,16 @@ public int getDownsampleFactor() { return downsampleFactor; } + @Override + public void close() { + final @Nullable Future currentRun = latestScheduledRun; + if (currentRun != null) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + currentRun.cancel(true); + } + } + } + void checkHealth() { if (isHealthy()) { if (downsampleFactor > 0) { @@ -61,11 +78,13 @@ void checkHealth() { private void reschedule(final int delay) { final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); if (!executorService.isClosed()) { - executorService.schedule(this, delay); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + latestScheduledRun = executorService.schedule(this, delay); + } } } private boolean isHealthy() { - return hub.isHealthy(); + return scopes.isHealthy(); } } diff --git a/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java index 05cf681950c..5577d67e3a5 100644 --- a/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java @@ -7,4 +7,6 @@ public interface IBackpressureMonitor { void start(); int getDownsampleFactor(); + + void close(); } diff --git a/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java index edbf660e24e..14a0db322c2 100644 --- a/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java @@ -19,4 +19,9 @@ public void start() { public int getDownsampleFactor() { return 0; } + + @Override + public void close() { + // do nothing + } } diff --git a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java index d48cc3108dd..479c0e42eaf 100644 --- a/sentry/src/main/java/io/sentry/cache/CacheStrategy.java +++ b/sentry/src/main/java/io/sentry/cache/CacheStrategy.java @@ -28,7 +28,6 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; -import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -242,7 +241,7 @@ private boolean isValidSession(final @NotNull Session session) { return false; } - final UUID sessionId = session.getSessionId(); + final String sessionId = session.getSessionId(); return sessionId != null; } diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 82636ac6c1b..8117002daf9 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -9,18 +9,21 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.DateUtils; import io.sentry.Hint; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryCrashLastRunState; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; import io.sentry.SentryItemType; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.SentryUUID; import io.sentry.Session; import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.hints.AbnormalExit; import io.sentry.hints.SessionEnd; import io.sentry.hints.SessionStart; import io.sentry.transport.NoOpEnvelopeCache; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.BufferedInputStream; @@ -43,7 +46,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -70,6 +72,7 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { private final CountDownLatch previousSessionLatch; private final @NotNull Map fileNameMap = new WeakHashMap<>(); + protected final @NotNull AutoClosableReentrantLock cacheLock = new AutoClosableReentrantLock(); public static @NotNull IEnvelopeCache create(final @NotNull SentryOptions options) { final String cacheDirPath = options.getCacheDirPath(); @@ -359,16 +362,18 @@ public void discard(final @NotNull SentryEnvelope envelope) { * @param envelope the SentryEnvelope object * @return the file */ - private synchronized @NotNull File getEnvelopeFile(final @NotNull SentryEnvelope envelope) { - final @NotNull String fileName; - if (fileNameMap.containsKey(envelope)) { - fileName = fileNameMap.get(envelope); - } else { - fileName = UUID.randomUUID() + SUFFIX_ENVELOPE_FILE; - fileNameMap.put(envelope, fileName); - } + private @NotNull File getEnvelopeFile(final @NotNull SentryEnvelope envelope) { + try (final @NotNull ISentryLifecycleToken ignored = cacheLock.acquire()) { + final @NotNull String fileName; + if (fileNameMap.containsKey(envelope)) { + fileName = fileNameMap.get(envelope); + } else { + fileName = SentryUUID.generateSentryId() + SUFFIX_ENVELOPE_FILE; + fileNameMap.put(envelope, fileName); + } - return new File(directory.getAbsolutePath(), fileName); + return new File(directory.getAbsolutePath(), fileName); + } } public static @NotNull File getCurrentSessionFile(final @NotNull String cacheDirPath) { diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index a88df2824fd..b4d4574abae 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -165,9 +165,6 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.Profile.equals(itemType)) { return DataCategory.Profile; } - if (SentryItemType.Statsd.equals(itemType)) { - return DataCategory.MetricBucket; - } if (SentryItemType.Attachment.equals(itemType)) { return DataCategory.Attachment; } diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java index 956996ce04b..aa690f14208 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java @@ -1,6 +1,6 @@ package io.sentry.instrumentation.file; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryOptions; @@ -28,8 +28,8 @@ final class FileIOSpanManager { private final @NotNull SentryStackTraceFactory stackTraceFactory; - static @Nullable ISpan startSpan(final @NotNull IHub hub, final @NotNull String op) { - final ISpan parent = Platform.isAndroid() ? hub.getTransaction() : hub.getSpan(); + static @Nullable ISpan startSpan(final @NotNull IScopes scopes, final @NotNull String op) { + final ISpan parent = Platform.isAndroid() ? scopes.getTransaction() : scopes.getSpan(); return parent != null ? parent.startChild(op) : null; } @@ -93,16 +93,16 @@ private void finishSpan() { if (currentSpan != null) { final String byteCountToString = StringUtils.byteCountToString(byteCount); if (file != null) { - final String description = file.getName() + " " + "(" + byteCountToString + ")"; + final String description = getDescription(file); currentSpan.setDescription(description); - if (Platform.isAndroid() || options.isSendDefaultPii()) { + if (options.isSendDefaultPii()) { currentSpan.setData("file.path", file.getAbsolutePath()); } } else { currentSpan.setDescription(byteCountToString); } currentSpan.setData("file.size", byteCount); - final boolean isMainThread = options.getMainThreadChecker().isMainThread(); + final boolean isMainThread = options.getThreadChecker().isMainThread(); currentSpan.setData(SpanDataConvention.BLOCKED_MAIN_THREAD_KEY, isMainThread); if (isMainThread) { currentSpan.setData( @@ -112,6 +112,22 @@ private void finishSpan() { } } + private @NotNull String getDescription(final @NotNull File file) { + final String byteCountToString = StringUtils.byteCountToString(byteCount); + // if we send PII, we can send the file name directly + if (options.isSendDefaultPii()) { + return file.getName() + " (" + byteCountToString + ")"; + } + final int lastDotIndex = file.getName().lastIndexOf('.'); + // if the file has an extension, show it in the description, even without sending PII + if (lastDotIndex > 0 && lastDotIndex < file.getName().length() - 1) { + final String fileExtension = file.getName().substring(lastDotIndex); + return "***" + fileExtension + " (" + byteCountToString + ")"; + } else { + return "***" + " (" + byteCountToString + ")"; + } + } + /** * A task that returns a result and may throw an IOException. Implementors define a single method * with no arguments called {@code call}. diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileInputStream.java b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileInputStream.java index 04bb87ae7c2..ea7d7f09a54 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileInputStream.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileInputStream.java @@ -1,8 +1,8 @@ package io.sentry.instrumentation.file; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; +import io.sentry.ScopesAdapter; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -25,24 +25,24 @@ public final class SentryFileInputStream extends FileInputStream { private final @NotNull FileIOSpanManager spanManager; public SentryFileInputStream(final @Nullable String name) throws FileNotFoundException { - this(name != null ? new File(name) : null, HubAdapter.getInstance()); + this(name != null ? new File(name) : null, ScopesAdapter.getInstance()); } public SentryFileInputStream(final @Nullable File file) throws FileNotFoundException { - this(file, HubAdapter.getInstance()); + this(file, ScopesAdapter.getInstance()); } public SentryFileInputStream(final @NotNull FileDescriptor fdObj) { - this(fdObj, HubAdapter.getInstance()); + this(fdObj, ScopesAdapter.getInstance()); } - SentryFileInputStream(final @Nullable File file, final @NotNull IHub hub) + SentryFileInputStream(final @Nullable File file, final @NotNull IScopes scopes) throws FileNotFoundException { - this(init(file, null, hub)); + this(init(file, null, scopes)); } - SentryFileInputStream(final @NotNull FileDescriptor fdObj, final @NotNull IHub hub) { - this(init(fdObj, null, hub), fdObj); + SentryFileInputStream(final @NotNull FileDescriptor fdObj, final @NotNull IScopes scopes) { + this(init(fdObj, null, scopes), fdObj); } private SentryFileInputStream( @@ -60,24 +60,24 @@ private SentryFileInputStream(final @NotNull FileInputStreamInitData data) } private static FileInputStreamInitData init( - final @Nullable File file, @Nullable FileInputStream delegate, final @NotNull IHub hub) + final @Nullable File file, @Nullable FileInputStream delegate, final @NotNull IScopes scopes) throws FileNotFoundException { - final ISpan span = FileIOSpanManager.startSpan(hub, "file.read"); + final ISpan span = FileIOSpanManager.startSpan(scopes, "file.read"); if (delegate == null) { delegate = new FileInputStream(file); } - return new FileInputStreamInitData(file, span, delegate, hub.getOptions()); + return new FileInputStreamInitData(file, span, delegate, scopes.getOptions()); } private static FileInputStreamInitData init( final @NotNull FileDescriptor fd, @Nullable FileInputStream delegate, - final @NotNull IHub hub) { - final ISpan span = FileIOSpanManager.startSpan(hub, "file.read"); + final @NotNull IScopes scopes) { + final ISpan span = FileIOSpanManager.startSpan(scopes, "file.read"); if (delegate == null) { delegate = new FileInputStream(fd); } - return new FileInputStreamInitData(null, span, delegate, hub.getOptions()); + return new FileInputStreamInitData(null, span, delegate, scopes.getOptions()); } @Override @@ -128,25 +128,27 @@ public static FileInputStream create( final @NotNull FileInputStream delegate, final @Nullable String name) throws FileNotFoundException { return new SentryFileInputStream( - init(name != null ? new File(name) : null, delegate, HubAdapter.getInstance())); + init(name != null ? new File(name) : null, delegate, ScopesAdapter.getInstance())); } public static FileInputStream create( final @NotNull FileInputStream delegate, final @Nullable File file) throws FileNotFoundException { - return new SentryFileInputStream(init(file, delegate, HubAdapter.getInstance())); + return new SentryFileInputStream(init(file, delegate, ScopesAdapter.getInstance())); } public static FileInputStream create( final @NotNull FileInputStream delegate, final @NotNull FileDescriptor descriptor) { return new SentryFileInputStream( - init(descriptor, delegate, HubAdapter.getInstance()), descriptor); + init(descriptor, delegate, ScopesAdapter.getInstance()), descriptor); } static FileInputStream create( - final @NotNull FileInputStream delegate, final @Nullable File file, final @NotNull IHub hub) + final @NotNull FileInputStream delegate, + final @Nullable File file, + final @NotNull IScopes scopes) throws FileNotFoundException { - return new SentryFileInputStream(init(file, delegate, hub)); + return new SentryFileInputStream(init(file, delegate, scopes)); } } } diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileOutputStream.java b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileOutputStream.java index 9424710d71d..4ef5022e1c9 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileOutputStream.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileOutputStream.java @@ -1,8 +1,8 @@ package io.sentry.instrumentation.file; -import io.sentry.HubAdapter; -import io.sentry.IHub; +import io.sentry.IScopes; import io.sentry.ISpan; +import io.sentry.ScopesAdapter; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -24,30 +24,31 @@ public final class SentryFileOutputStream extends FileOutputStream { private final @NotNull FileIOSpanManager spanManager; public SentryFileOutputStream(final @Nullable String name) throws FileNotFoundException { - this(name != null ? new File(name) : null, false, HubAdapter.getInstance()); + this(name != null ? new File(name) : null, false, ScopesAdapter.getInstance()); } public SentryFileOutputStream(final @Nullable String name, final boolean append) throws FileNotFoundException { - this(init(name != null ? new File(name) : null, append, null, HubAdapter.getInstance())); + this(init(name != null ? new File(name) : null, append, null, ScopesAdapter.getInstance())); } public SentryFileOutputStream(final @Nullable File file) throws FileNotFoundException { - this(file, false, HubAdapter.getInstance()); + this(file, false, ScopesAdapter.getInstance()); } public SentryFileOutputStream(final @Nullable File file, final boolean append) throws FileNotFoundException { - this(init(file, append, null, HubAdapter.getInstance())); + this(init(file, append, null, ScopesAdapter.getInstance())); } public SentryFileOutputStream(final @NotNull FileDescriptor fdObj) { - this(init(fdObj, null, HubAdapter.getInstance()), fdObj); + this(init(fdObj, null, ScopesAdapter.getInstance()), fdObj); } - SentryFileOutputStream(final @Nullable File file, final boolean append, final @NotNull IHub hub) + SentryFileOutputStream( + final @Nullable File file, final boolean append, final @NotNull IScopes scopes) throws FileNotFoundException { - this(init(file, append, null, hub)); + this(init(file, append, null, scopes)); } private SentryFileOutputStream( @@ -68,22 +69,24 @@ private static FileOutputStreamInitData init( final @Nullable File file, final boolean append, @Nullable FileOutputStream delegate, - @NotNull IHub hub) + @NotNull IScopes scopes) throws FileNotFoundException { - final ISpan span = FileIOSpanManager.startSpan(hub, "file.write"); + final ISpan span = FileIOSpanManager.startSpan(scopes, "file.write"); if (delegate == null) { delegate = new FileOutputStream(file, append); } - return new FileOutputStreamInitData(file, append, span, delegate, hub.getOptions()); + return new FileOutputStreamInitData(file, append, span, delegate, scopes.getOptions()); } private static FileOutputStreamInitData init( - final @NotNull FileDescriptor fd, @Nullable FileOutputStream delegate, @NotNull IHub hub) { - final ISpan span = FileIOSpanManager.startSpan(hub, "file.write"); + final @NotNull FileDescriptor fd, + @Nullable FileOutputStream delegate, + @NotNull IScopes scopes) { + final ISpan span = FileIOSpanManager.startSpan(scopes, "file.write"); if (delegate == null) { delegate = new FileOutputStream(fd); } - return new FileOutputStreamInitData(null, false, span, delegate, hub.getOptions()); + return new FileOutputStreamInitData(null, false, span, delegate, scopes.getOptions()); } @Override @@ -132,31 +135,32 @@ public static FileOutputStream create( final @NotNull FileOutputStream delegate, final @Nullable String name) throws FileNotFoundException { return new SentryFileOutputStream( - init(name != null ? new File(name) : null, false, delegate, HubAdapter.getInstance())); + init(name != null ? new File(name) : null, false, delegate, ScopesAdapter.getInstance())); } public static FileOutputStream create( final @NotNull FileOutputStream delegate, final @Nullable String name, final boolean append) throws FileNotFoundException { return new SentryFileOutputStream( - init(name != null ? new File(name) : null, append, delegate, HubAdapter.getInstance())); + init( + name != null ? new File(name) : null, append, delegate, ScopesAdapter.getInstance())); } public static FileOutputStream create( final @NotNull FileOutputStream delegate, final @Nullable File file) throws FileNotFoundException { - return new SentryFileOutputStream(init(file, false, delegate, HubAdapter.getInstance())); + return new SentryFileOutputStream(init(file, false, delegate, ScopesAdapter.getInstance())); } public static FileOutputStream create( final @NotNull FileOutputStream delegate, final @Nullable File file, final boolean append) throws FileNotFoundException { - return new SentryFileOutputStream(init(file, append, delegate, HubAdapter.getInstance())); + return new SentryFileOutputStream(init(file, append, delegate, ScopesAdapter.getInstance())); } public static FileOutputStream create( final @NotNull FileOutputStream delegate, final @NotNull FileDescriptor fdObj) { - return new SentryFileOutputStream(init(fdObj, delegate, HubAdapter.getInstance()), fdObj); + return new SentryFileOutputStream(init(fdObj, delegate, ScopesAdapter.getInstance()), fdObj); } } } diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileReader.java b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileReader.java index 0a225e65a58..38a83c7ff69 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileReader.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileReader.java @@ -1,6 +1,6 @@ package io.sentry.instrumentation.file; -import io.sentry.IHub; +import io.sentry.IScopes; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -20,7 +20,8 @@ public SentryFileReader(final @NotNull FileDescriptor fd) { super(new SentryFileInputStream(fd)); } - SentryFileReader(final @NotNull File file, final @NotNull IHub hub) throws FileNotFoundException { - super(new SentryFileInputStream(file, hub)); + SentryFileReader(final @NotNull File file, final @NotNull IScopes scopes) + throws FileNotFoundException { + super(new SentryFileInputStream(file, scopes)); } } diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileWriter.java b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileWriter.java index 95889846124..93c901ec6c7 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileWriter.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/SentryFileWriter.java @@ -1,6 +1,6 @@ package io.sentry.instrumentation.file; -import io.sentry.IHub; +import io.sentry.IScopes; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -30,8 +30,8 @@ public SentryFileWriter(final @NotNull FileDescriptor fd) { super(new SentryFileOutputStream(fd)); } - SentryFileWriter(final @NotNull File file, final boolean append, final @NotNull IHub hub) + SentryFileWriter(final @NotNull File file, final boolean append, final @NotNull IScopes scopes) throws FileNotFoundException { - super(new SentryFileOutputStream(file, append, hub)); + super(new SentryFileOutputStream(file, append, scopes)); } } diff --git a/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java b/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java new file mode 100644 index 00000000000..1f504f25557 --- /dev/null +++ b/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java @@ -0,0 +1,34 @@ +package io.sentry.internal.eventprocessor; + +import io.sentry.EventProcessor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class EventProcessorAndOrder implements Comparable { + + private final @NotNull EventProcessor eventProcessor; + private final @NotNull Long order; + + public EventProcessorAndOrder( + final @NotNull EventProcessor eventProcessor, final @Nullable Long order) { + this.eventProcessor = eventProcessor; + if (order == null) { + this.order = System.nanoTime(); + } else { + this.order = order; + } + } + + public @NotNull EventProcessor getEventProcessor() { + return eventProcessor; + } + + public @NotNull Long getOrder() { + return order; + } + + @Override + public int compareTo(@NotNull EventProcessorAndOrder o) { + return order.compareTo(o.order); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java deleted file mode 100644 index 3861387f0d9..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import java.util.Collections; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** Counters track a value that can only be incremented. */ -@ApiStatus.Internal -public final class CounterMetric extends Metric { - private double value; - - public CounterMetric( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - super(MetricType.Counter, key, unit, tags); - this.value = value; - } - - public double getValue() { - return value; - } - - @Override - public void add(final double value) { - this.value += value; - } - - @Override - public int getWeight() { - return 1; - } - - @Override - public @NotNull Iterable serialize() { - return Collections.singletonList(value); - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java deleted file mode 100644 index 17417d150d5..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class DistributionMetric extends Metric { - - private final List values = new ArrayList<>(); - - public DistributionMetric( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - super(MetricType.Distribution, key, unit, tags); - this.values.add(value); - } - - @Override - public void add(final double value) { - values.add(value); - } - - @Override - public int getWeight() { - return values.size(); - } - - @Override - public @NotNull Iterable serialize() { - return values; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java deleted file mode 100644 index e200df0cd98..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.sentry.metrics; - -import java.nio.charset.Charset; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.TestOnly; - -/** - * EncodedMetrics is a class that represents a collection of aggregated metrics, grouped by buckets. - */ -@ApiStatus.Internal -public final class EncodedMetrics { - @SuppressWarnings({"CharsetObjectCanBeUsed"}) - private static final Charset UTF8 = Charset.forName("UTF-8"); - - private final Map> buckets; - - public EncodedMetrics(final @NotNull Map> buckets) { - this.buckets = buckets; - } - - /** - * Encodes the metrics into a Statsd compatible format. - * - *

See github.com/statsd/statsd#usage and - * getsentry.github.io/relay/relay_metrics/index.html - * for more details about the format. - * - * @return the encoded metrics - */ - public byte[] encodeToStatsd() { - final StringBuilder statsd = new StringBuilder(); - for (Map.Entry> entry : buckets.entrySet()) { - MetricsHelper.encodeMetrics(entry.getKey(), entry.getValue().values(), statsd); - } - return statsd.toString().getBytes(UTF8); - } - - @TestOnly - Map> getBuckets() { - return buckets; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java deleted file mode 100644 index 6f54e383c2f..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import java.util.Arrays; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** Gauges track a value that can go up and down. */ -@ApiStatus.Internal -public final class GaugeMetric extends Metric { - - private double last; - private double min; - private double max; - private double sum; - private int count; - - public GaugeMetric( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - super(MetricType.Gauge, key, unit, tags); - - this.last = value; - this.min = value; - this.max = value; - this.sum = value; - this.count = 1; - } - - @Override - public void add(final double value) { - this.last = value; - min = Math.min(min, value); - max = Math.max(max, value); - sum += value; - count++; - } - - public double getLast() { - return last; - } - - public double getMin() { - return min; - } - - public double getMax() { - return max; - } - - public double getSum() { - return sum; - } - - public int getCount() { - return count; - } - - @Override - public int getWeight() { - return 5; - } - - @Override - public @NotNull Iterable serialize() { - return Arrays.asList(last, min, max, sum, count); - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java b/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java deleted file mode 100644 index e3ad302eb68..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.protocol.SentryId; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public interface IMetricsClient { - /** Captures one or more metrics to be sent to Sentry. */ - @NotNull - SentryId captureMetrics(final @NotNull EncodedMetrics metrics); -} diff --git a/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java deleted file mode 100644 index 2afbf8e19e3..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/LocalMetricsAggregator.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import io.sentry.protocol.MetricSummary; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Correlates metrics to spans. See the RFC - * for more details. - */ -@ApiStatus.Internal -public final class LocalMetricsAggregator { - - // format: > - private final @NotNull Map> buckets = new HashMap<>(); - - public void add( - final @NotNull String bucketKey, - final @NotNull MetricType type, - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - final @NotNull String exportKey = MetricsHelper.getExportKey(type, key, unit); - - synchronized (buckets) { - @Nullable Map bucket = buckets.get(exportKey); - //noinspection Java8MapApi - if (bucket == null) { - bucket = new HashMap<>(); - buckets.put(exportKey, bucket); - } - - @Nullable GaugeMetric gauge = bucket.get(bucketKey); - if (gauge == null) { - gauge = new GaugeMetric(key, value, unit, tags); - bucket.put(bucketKey, gauge); - } else { - gauge.add(value); - } - } - } - - @NotNull - public Map> getSummaries() { - final @NotNull Map> summaries = new HashMap<>(); - synchronized (buckets) { - for (final @NotNull Map.Entry> entry : buckets.entrySet()) { - final @NotNull String exportKey = Objects.requireNonNull(entry.getKey()); - final @NotNull List metricSummaries = new ArrayList<>(); - for (@NotNull GaugeMetric gauge : entry.getValue().values()) { - metricSummaries.add( - new MetricSummary( - gauge.getMin(), - gauge.getMax(), - gauge.getSum(), - gauge.getCount(), - gauge.getTags())); - } - summaries.put(exportKey, metricSummaries); - } - } - return summaries; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java deleted file mode 100644 index 57bbd9c22cb..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/Metric.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** Base class for metric instruments */ -@ApiStatus.Internal -public abstract class Metric { - - private final @NotNull MetricType type; - private final @NotNull String key; - private final @Nullable MeasurementUnit unit; - private final @Nullable Map tags; - - /** - * Creates a new instance of {@link Metric}. - * - * @param key The text key to be used to identify the metric - * @param unit An optional {@link MeasurementUnit} that describes the values being tracked - * @param tags An optional set of key/value pairs that can be used to add dimensionality to - * metrics - */ - public Metric( - final @NotNull MetricType type, - final @NotNull String key, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - this.type = type; - this.key = key; - this.unit = unit; - this.tags = tags; - } - - /** Adds a value to the metric */ - public abstract void add(final double value); - - @NotNull - public MetricType getType() { - return type; - } - - public abstract int getWeight(); - - @NotNull - public String getKey() { - return key; - } - - @Nullable - public MeasurementUnit getUnit() { - return unit; - } - - @Nullable - public Map getTags() { - return tags; - } - - public abstract @NotNull Iterable serialize(); -} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricType.java b/sentry/src/main/java/io/sentry/metrics/MetricType.java deleted file mode 100644 index fc816bc25ac..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/MetricType.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.sentry.metrics; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** The metric instrument type */ -@ApiStatus.Internal -public enum MetricType { - Counter("c"), - Gauge("g"), - Distribution("d"), - Set("s"); - - final @NotNull String statsdCode; - - MetricType(final @NotNull String statsdCode) { - this.statsdCode = statsdCode; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java deleted file mode 100644 index 45273d6885e..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ /dev/null @@ -1,464 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.IMetricsAggregator; -import io.sentry.ISpan; -import io.sentry.MeasurementUnit; -import io.sentry.SentryDate; -import io.sentry.SentryNanotimeDate; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class MetricsApi { - - @ApiStatus.Internal - public interface IMetricsInterface { - @NotNull - IMetricsAggregator getMetricsAggregator(); - - @Nullable - LocalMetricsAggregator getLocalMetricsAggregator(); - - @NotNull - Map getDefaultTagsForMetrics(); - - @Nullable - ISpan startSpanForMetric(final @NotNull String op, final @NotNull String description); - } - - private final @NotNull MetricsApi.IMetricsInterface aggregator; - - public MetricsApi(final @NotNull MetricsApi.IMetricsInterface aggregator) { - this.aggregator = aggregator; - } - - /** - * Emits an increment of 1.0 for a counter - * - * @param key A unique key identifying the metric - */ - public void increment(final @NotNull String key) { - increment(key, 1.0, null, null, null); - } - - /** - * Emits a Counter metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - */ - public void increment(final @NotNull String key, final double value) { - increment(key, value, null, null, null); - } - - /** - * Emits a Counter metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - */ - public void increment( - final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { - - increment(key, value, unit, null, null); - } - - /** - * Emits a Counter metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - */ - public void increment( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - increment(key, value, unit, tags, null); - } - - /** - * Emits a Counter metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted. Defaults to the time at which the - * metric is emitted, if no value is provided. - */ - public void increment( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final @Nullable Long timestampMs) { - - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); - - aggregator - .getMetricsAggregator() - .increment(key, value, unit, enrichedTags, timestamp, localMetricsAggregator); - } - - /** - * Emits a Gauge metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - */ - public void gauge(final @NotNull String key, final double value) { - gauge(key, value, null, null, null); - } - - /** - * Emits a Gauge metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - */ - public void gauge( - final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { - gauge(key, value, unit, null, null); - } - - /** - * Emits a Gauge metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - */ - public void gauge( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - gauge(key, value, unit, tags, null); - } - - /** - * Emits a Gauge metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted. Defaults to the time at which the - * metric is emitted, if no value is provided. - */ - public void gauge( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final @Nullable Long timestampMs) { - - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); - - aggregator - .getMetricsAggregator() - .gauge(key, value, unit, enrichedTags, timestamp, localMetricsAggregator); - } - - /** - * Emits a Distribution metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - */ - public void distribution(final @NotNull String key, final double value) { - distribution(key, value, null, null, null); - } - - /** - * Emits a Distribution metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - */ - public void distribution( - final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { - - distribution(key, value, unit, null, null); - } - - /** - * Emits a Distribution metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - */ - public void distribution( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - distribution(key, value, unit, tags, null); - } - - /** - * Emits a Distribution metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted. Defaults to the time at which the - * metric is emitted, if no value is provided. - */ - public void distribution( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final @Nullable Long timestampMs) { - - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); - - aggregator - .getMetricsAggregator() - .distribution(key, value, unit, enrichedTags, timestamp, localMetricsAggregator); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - */ - public void set(final @NotNull String key, final int value) { - set(key, value, null, null, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - */ - public void set( - final @NotNull String key, final int value, final @Nullable MeasurementUnit unit) { - - set(key, value, unit, null, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - */ - public void set( - final @NotNull String key, - final int value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - set(key, value, unit, tags, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted. Defaults to the time at which the - * metric is emitted, if no value is provided. - */ - public void set( - final @NotNull String key, - final int value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final @Nullable Long timestampMs) { - - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); - - aggregator - .getMetricsAggregator() - .set(key, value, unit, enrichedTags, timestamp, localMetricsAggregator); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - */ - public void set(final @NotNull String key, final @NotNull String value) { - set(key, value, null, null, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - */ - public void set( - final @NotNull String key, - final @NotNull String value, - final @Nullable MeasurementUnit unit) { - - set(key, value, unit, null, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - */ - public void set( - final @NotNull String key, - final @NotNull String value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - - set(key, value, unit, tags, null); - } - - /** - * Emits a Set metric - * - * @param key A unique key identifying the metric - * @param value The value to be added - * @param unit An optional unit, see {@link MeasurementUnit} - * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted. Defaults to the time at which the - * metric is emitted, if no value is provided. - */ - public void set( - final @NotNull String key, - final @NotNull String value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final @Nullable Long timestampMs) { - - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator = - aggregator.getLocalMetricsAggregator(); - - aggregator - .getMetricsAggregator() - .set(key, value, unit, enrichedTags, timestamp, localMetricsAggregator); - } - - /** - * Emits a distribution with the time it takes to run a given code block. - * - * @param key A unique key identifying the metric - * @param callback The code block to measure - */ - public void timing(final @NotNull String key, final @NotNull Runnable callback) { - - timing(key, callback, null, null); - } - - /** - * Emits a distribution with the time it takes to run a given code block. - * - * @param key A unique key identifying the metric - * @param callback The code block to measure - * @param unit An optional unit, see {@link MeasurementUnit.Duration} - */ - public void timing( - final @NotNull String key, - final @NotNull Runnable callback, - final @NotNull MeasurementUnit.Duration unit) { - - timing(key, callback, unit, null); - } - - /** - * Emits a distribution with the time it takes to run a given code block. - * - * @param key A unique key identifying the metric - * @param callback The code block to measure - * @param unit An optional unit, see {@link MeasurementUnit.Duration} - * @param tags Optional Tags to associate with the metric - */ - public void timing( - final @NotNull String key, - final @NotNull Runnable callback, - final @Nullable MeasurementUnit.Duration unit, - final @Nullable Map tags) { - - final @NotNull MeasurementUnit.Duration durationUnit = - unit != null ? unit : MeasurementUnit.Duration.SECOND; - final @NotNull Map enrichedTags = - MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); - final @Nullable LocalMetricsAggregator localMetricsAggregator; - - final @Nullable ISpan span = aggregator.startSpanForMetric("metric.timing", key); - // If the span was started, we take its local aggregator, otherwise we request another one. - localMetricsAggregator = - span != null ? span.getLocalMetricsAggregator() : aggregator.getLocalMetricsAggregator(); - - if (span != null && tags != null) { - for (final @NotNull Map.Entry entry : tags.entrySet()) { - span.setTag(entry.getKey(), entry.getValue()); - } - } - final long timestamp = System.currentTimeMillis(); - final long startNanos = System.nanoTime(); - try { - callback.run(); - } finally { - // If we have a span, the duration we emit is the same of the span, otherwise calculate it. - final long durationNanos; - if (span != null) { - span.finish(); - // We finish the span, so we should have a finish date, but it's still nullable. - final @NotNull SentryDate spanFinishDate = - span.getFinishDate() != null ? span.getFinishDate() : new SentryNanotimeDate(); - durationNanos = spanFinishDate.diff(span.getStartDate()); - } else { - durationNanos = System.nanoTime() - startNanos; - } - final double value = MetricsHelper.convertNanosTo(durationUnit, durationNanos); - aggregator - .getMetricsAggregator() - .distribution(key, value, durationUnit, enrichedTags, timestamp, localMetricsAggregator); - } - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java deleted file mode 100644 index 37fd4523a4d..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ /dev/null @@ -1,267 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import io.sentry.util.Random; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Pattern; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; - -@ApiStatus.Internal -public final class MetricsHelper { - public static final long FLUSHER_SLEEP_TIME_MS = 5000; - public static final int MAX_TOTAL_WEIGHT = 100000; - private static final int ROLLUP_IN_SECONDS = 10; - - private static final Pattern UNIT_PATTERN = Pattern.compile("\\W+"); - private static final Pattern NAME_PATTERN = Pattern.compile("[^\\w\\-.]+"); - private static final Pattern TAG_KEY_PATTERN = Pattern.compile("[^\\w\\-./]+"); - - private static final char TAGS_PAIR_DELIMITER = ','; // Delimiter between key-value pairs - private static final char TAGS_KEY_VALUE_DELIMITER = '='; // Delimiter between key and value - private static final char TAGS_ESCAPE_CHAR = '\\'; - - private static long FLUSH_SHIFT_MS = - (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); - - public static long getTimeBucketKey(final long timestampMs) { - final long seconds = timestampMs / 1000; - final long bucketKey = (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; - // this will result into timestamps of -9999...9999 to fall into a ~20s bucket - // simply shift the bucket key for negative timestamp values to ensure those two are apart - if (timestampMs >= 0) { - return bucketKey; - } else return bucketKey - 1; - } - - public static long getCutoffTimestampMs(final long nowMs) { - return nowMs - (ROLLUP_IN_SECONDS * 1000) - FLUSH_SHIFT_MS; - } - - @NotNull - public static String sanitizeUnit(final @NotNull String unit) { - return UNIT_PATTERN.matcher(unit).replaceAll(""); - } - - @NotNull - public static String sanitizeName(final @NotNull String input) { - return NAME_PATTERN.matcher(input).replaceAll("_"); - } - - @NotNull - public static String sanitizeTagKey(final @NotNull String input) { - return TAG_KEY_PATTERN.matcher(input).replaceAll(""); - } - - @NotNull - public static String sanitizeTagValue(final @NotNull String input) { - // see https://develop.sentry.dev/sdk/metrics/#tag-values-replacement-map - // Line feed -> \n - // Carriage return -> \r - // Tab -> \t - // Backslash -> \\ - // Pipe -> \\u{7c} - // Comma -> \\u{2c} - final StringBuilder output = new StringBuilder(input.length()); - for (int idx = 0; idx < input.length(); idx++) { - final char ch = input.charAt(idx); - if (ch == '\n') { - output.append("\\n"); - } else if (ch == '\r') { - output.append("\\r"); - } else if (ch == '\t') { - output.append("\\t"); - } else if (ch == '\\') { - output.append("\\\\"); - } else if (ch == '|') { - output.append("\\u{7c}"); - } else if (ch == ',') { - output.append("\\u{2c}"); - } else { - output.append(ch); - } - } - return output.toString(); - } - - @NotNull - public static String getMetricBucketKey( - final @NotNull MetricType type, - final @NotNull String metricKey, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - final @NotNull String typePrefix = type.statsdCode; - final @NotNull String serializedTags = getTagsKey(tags); - - final @NotNull String unitName = getUnitName(unit); - return String.format("%s_%s_%s_%s", typePrefix, metricKey, unitName, serializedTags); - } - - @NotNull - private static String getUnitName(final @Nullable MeasurementUnit unit) { - return (unit != null) ? unit.apiName() : MeasurementUnit.NONE; - } - - @NotNull - private static String getTagsKey(final @Nullable Map tags) { - if (tags == null || tags.isEmpty()) { - return ""; - } - - final @NotNull StringBuilder builder = new StringBuilder(); - for (Map.Entry tag : tags.entrySet()) { - - // Escape delimiters in key and value - final @NotNull String key = escapeString(tag.getKey()); - final @NotNull String value = escapeString(tag.getValue()); - - if (builder.length() > 0) { - builder.append(TAGS_PAIR_DELIMITER); - } - - builder.append(key).append(TAGS_KEY_VALUE_DELIMITER).append(value); - } - - return builder.toString(); - } - - @NotNull - private static String escapeString(final @NotNull String input) { - final StringBuilder escapedString = new StringBuilder(input.length()); - - for (int idx = 0; idx < input.length(); idx++) { - final char ch = input.charAt(idx); - - if (ch == TAGS_PAIR_DELIMITER || ch == TAGS_KEY_VALUE_DELIMITER) { - escapedString.append(TAGS_ESCAPE_CHAR); // Prefix with escape character - } - escapedString.append(ch); - } - - return escapedString.toString(); - } - - /** - * Provides an export key for identifying the metric without its tags. Suitable for span level - * metric summaries - * - * @param type the metric type - * @param key the metric key - * @param unit the metric unit - * @return the export key - */ - @NotNull - public static String getExportKey( - final @NotNull MetricType type, - final @NotNull String key, - final @Nullable MeasurementUnit unit) { - final @NotNull String unitName = getUnitName(unit); - return String.format("%s:%s@%s", type.statsdCode, key, unitName); - } - - public static double convertNanosTo( - final @NotNull MeasurementUnit.Duration unit, final long durationNanos) { - switch (unit) { - case NANOSECOND: - return durationNanos; - case MICROSECOND: - return (double) durationNanos / 1000.0d; - case MILLISECOND: - return (double) durationNanos / 1000000.0d; - case SECOND: - return (double) durationNanos / 1000000000.0d; - case MINUTE: - return (double) durationNanos / 60000000000.0d; - case HOUR: - return (double) durationNanos / 3600000000000.0d; - case DAY: - return (double) durationNanos / 86400000000000.0d; - case WEEK: - return (double) durationNanos / 86400000000000.0d / 7.0d; - default: - throw new IllegalArgumentException("Unknown Duration unit: " + unit.name()); - } - } - - /** - * Encodes the metrics - * - *

See github.com/statsd/statsd#usage and - * getsentry.github.io/relay/relay_metrics/index.html - * for more details about the format. - * - * @param timestamp The bucket time the metrics belong to, in second resolution - * @param metrics The metrics to encode - * @param writer The writer to encode the metrics into - */ - public static void encodeMetrics( - final long timestamp, - final @NotNull Collection metrics, - final @NotNull StringBuilder writer) { - for (Metric metric : metrics) { - writer.append(sanitizeName(metric.getKey())); - writer.append("@"); - - final @Nullable MeasurementUnit unit = metric.getUnit(); - final @NotNull String unitName = getUnitName(unit); - final String sanitizeUnitName = sanitizeUnit(unitName); - writer.append(sanitizeUnitName); - - for (final @NotNull Object value : metric.serialize()) { - writer.append(":"); - writer.append(value); - } - - writer.append("|"); - writer.append(metric.getType().statsdCode); - - final @Nullable Map tags = metric.getTags(); - if (tags != null) { - writer.append("|#"); - boolean first = true; - for (final @NotNull Map.Entry tag : tags.entrySet()) { - final @NotNull String tagKey = sanitizeTagKey(tag.getKey()); - if (first) { - first = false; - } else { - writer.append(","); - } - writer.append(tagKey); - writer.append(":"); - writer.append(sanitizeTagValue(tag.getValue())); - } - } - - writer.append("|T"); - writer.append(timestamp); - writer.append("\n"); - } - } - - @NotNull - public static Map mergeTags( - final @Nullable Map tags, final @NotNull Map defaultTags) { - if (tags == null) { - return Collections.unmodifiableMap(defaultTags); - } - final @NotNull Map enrichedTags = new HashMap<>(tags); - for (final @NotNull Map.Entry defaultTag : defaultTags.entrySet()) { - final @NotNull String key = defaultTag.getKey(); - if (!enrichedTags.containsKey(key)) { - enrichedTags.put(key, defaultTag.getValue()); - } - } - return enrichedTags; - } - - @TestOnly - public static void setFlushShiftMs(long flushShiftMs) { - FLUSH_SHIFT_MS = flushShiftMs; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java deleted file mode 100644 index f9038062b5c..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.IMetricsAggregator; -import io.sentry.ISpan; -import io.sentry.MeasurementUnit; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class NoopMetricsAggregator - implements IMetricsAggregator, MetricsApi.IMetricsInterface { - - private static final NoopMetricsAggregator instance = new NoopMetricsAggregator(); - - public static NoopMetricsAggregator getInstance() { - return instance; - } - - @Override - public void increment( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - // no-op - } - - @Override - public void gauge( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - // no-op - } - - @Override - public void distribution( - final @NotNull String key, - final double value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - // no-op - } - - @Override - public void set( - final @NotNull String key, - final int value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - // no-op - } - - @Override - public void set( - final @NotNull String key, - final @NotNull String value, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags, - final long timestampMs, - final @Nullable LocalMetricsAggregator localMetricsAggregator) { - // no-op - } - - @Override - public void flush(final boolean force) { - // no-op - } - - @Override - public void close() throws IOException {} - - @Override - public @NotNull IMetricsAggregator getMetricsAggregator() { - return this; - } - - @Override - public @Nullable LocalMetricsAggregator getLocalMetricsAggregator() { - return null; - } - - @Override - public @NotNull Map getDefaultTagsForMetrics() { - return Collections.emptyMap(); - } - - @Override - public @Nullable ISpan startSpanForMetric(@NotNull String op, @NotNull String description) { - return null; - } -} diff --git a/sentry/src/main/java/io/sentry/metrics/SetMetric.java b/sentry/src/main/java/io/sentry/metrics/SetMetric.java deleted file mode 100644 index 43d16ed5503..00000000000 --- a/sentry/src/main/java/io/sentry/metrics/SetMetric.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.sentry.metrics; - -import io.sentry.MeasurementUnit; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** Sets track a set of values on which you can perform aggregations such as count_unique. */ -@ApiStatus.Internal -public final class SetMetric extends Metric { - - private final @NotNull Set values = new HashSet<>(); - - public SetMetric( - final @NotNull String key, - final @Nullable MeasurementUnit unit, - final @Nullable Map tags) { - super(MetricType.Set, key, unit, tags); - } - - /** - * Adds a value to the set. Note: the value will be truncated to an integer. - * - * @param value the value to add to the set. - */ - @Override - public void add(final double value) { - values.add((int) value); - } - - @Override - public int getWeight() { - return values.size(); - } - - @Override - public @NotNull Iterable serialize() { - return values; - } -} diff --git a/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java b/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java new file mode 100644 index 00000000000..b491ab5278d --- /dev/null +++ b/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java @@ -0,0 +1,50 @@ +package io.sentry.opentelemetry; + +import io.sentry.SentryOpenTelemetryMode; +import io.sentry.SentryOptions; +import io.sentry.util.LoadClass; +import io.sentry.util.Platform; +import io.sentry.util.SpanUtils; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class OpenTelemetryUtil { + + @ApiStatus.Internal + public static void applyIgnoredSpanOrigins( + final @NotNull SentryOptions options, final @NotNull LoadClass loadClass) { + if (Platform.isJvm()) { + final @NotNull List ignored = ignoredSpanOrigins(options, loadClass); + for (String origin : ignored) { + options.addIgnoredSpanOrigin(origin); + } + } + } + + private static @NotNull List ignoredSpanOrigins( + final @NotNull SentryOptions options, final @NotNull LoadClass loadClass) { + final @NotNull SentryOpenTelemetryMode openTelemetryMode = options.getOpenTelemetryMode(); + if (SentryOpenTelemetryMode.AUTO.equals(openTelemetryMode)) { + if (loadClass.isClassAvailable( + "io.sentry.opentelemetry.agent.AgentMarker", options.getLogger())) { + return SpanUtils.ignoredSpanOriginsForOpenTelemetry(SentryOpenTelemetryMode.AGENT); + } + if (loadClass.isClassAvailable( + "io.sentry.opentelemetry.agent.AgentlessMarker", options.getLogger())) { + return SpanUtils.ignoredSpanOriginsForOpenTelemetry(SentryOpenTelemetryMode.AGENTLESS); + } + if (loadClass.isClassAvailable( + "io.sentry.opentelemetry.agent.AgentlessSpringMarker", options.getLogger())) { + return SpanUtils.ignoredSpanOriginsForOpenTelemetry( + SentryOpenTelemetryMode.AGENTLESS_SPRING); + } + } else { + return SpanUtils.ignoredSpanOriginsForOpenTelemetry(openTelemetryMode); + } + + return Collections.emptyList(); + } +} diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index ba4cbe51cb6..53c97bcb0b0 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -1,28 +1,37 @@ package io.sentry.protocol; +import com.jakewharton.nopen.annotation.Open; import io.sentry.ILogger; +import io.sentry.ISentryLifecycleToken; import io.sentry.JsonDeserializer; import io.sentry.JsonSerializable; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; import io.sentry.SpanContext; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Collections; +import java.util.Enumeration; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class Contexts extends ConcurrentHashMap implements JsonSerializable { +@Open +public class Contexts implements JsonSerializable { private static final long serialVersionUID = 252445813254943011L; public static final String REPLAY_ID = "replay_id"; + private final @NotNull ConcurrentHashMap internalStorage = + new ConcurrentHashMap<>(); + /** Response lock, Ops should be atomic */ - private final @NotNull Object responseLock = new Object(); + protected final @NotNull AutoClosableReentrantLock responseLock = new AutoClosableReentrantLock(); public Contexts() {} @@ -63,7 +72,7 @@ public Contexts(final @NotNull Contexts contexts) { return toContextType(SpanContext.TYPE, SpanContext.class); } - public void setTrace(final @Nullable SpanContext traceContext) { + public void setTrace(final @NotNull SpanContext traceContext) { Objects.requireNonNull(traceContext, "traceContext is required"); this.put(SpanContext.TYPE, traceContext); } @@ -121,7 +130,7 @@ public void setGpu(final @NotNull Gpu gpu) { } public void withResponse(HintUtils.SentryConsumer callback) { - synchronized (responseLock) { + try (final @NotNull ISentryLifecycleToken ignored = responseLock.acquire()) { final @Nullable Response response = getResponse(); if (response != null) { callback.accept(response); @@ -134,11 +143,76 @@ public void withResponse(HintUtils.SentryConsumer callback) { } public void setResponse(final @NotNull Response response) { - synchronized (responseLock) { + try (final @NotNull ISentryLifecycleToken ignored = responseLock.acquire()) { this.put(Response.TYPE, response); } } + public int size() { + // since this used to extend map + return internalStorage.size(); + } + + public int getSize() { + // for kotlin .size + return size(); + } + + public boolean isEmpty() { + return internalStorage.isEmpty(); + } + + public boolean containsKey(final @NotNull Object key) { + return internalStorage.containsKey(key); + } + + public @Nullable Object get(final @NotNull Object key) { + return internalStorage.get(key); + } + + public @Nullable Object put(final @NotNull String key, final @Nullable Object value) { + return internalStorage.put(key, value); + } + + public @Nullable Object set(final @NotNull String key, final @Nullable Object value) { + return put(key, value); + } + + public @Nullable Object remove(final @NotNull Object key) { + return internalStorage.remove(key); + } + + public @NotNull Enumeration keys() { + return internalStorage.keys(); + } + + public @NotNull Set> entrySet() { + return internalStorage.entrySet(); + } + + public void putAll(Map m) { + internalStorage.putAll(m); + } + + public void putAll(final @NotNull Contexts contexts) { + internalStorage.putAll(contexts.internalStorage); + } + + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof Contexts) { + final @NotNull Contexts otherContexts = (Contexts) obj; + return internalStorage.equals(otherContexts.internalStorage); + } + + return false; + } + + @Override + public int hashCode() { + return internalStorage.hashCode(); + } + // region json @Override diff --git a/sentry/src/main/java/io/sentry/protocol/Device.java b/sentry/src/main/java/io/sentry/protocol/Device.java index 25cfa41fd13..02bb39f25b1 100644 --- a/sentry/src/main/java/io/sentry/protocol/Device.java +++ b/sentry/src/main/java/io/sentry/protocol/Device.java @@ -103,14 +103,6 @@ public final class Device implements JsonUnknown, JsonSerializable { private @Nullable String id; - /** - * This method returns the language code for this locale, which will either be the empty string or - * a lowercase ISO 639 code. - * - * @deprecated use {@link Device#getLocale()} - */ - @Deprecated private @Nullable String language; - /** The locale of the device. For example, en-US. */ private @Nullable String locale; @@ -162,7 +154,6 @@ public Device() {} this.screenDpi = device.screenDpi; this.bootTime = device.bootTime; this.id = device.id; - this.language = device.language; this.connectionType = device.connectionType; this.batteryTemperature = device.batteryTemperature; this.batteryLevel = device.batteryLevel; @@ -398,14 +389,6 @@ public void setId(final @Nullable String id) { this.id = id; } - public @Nullable String getLanguage() { - return language; - } - - public void setLanguage(final @Nullable String language) { - this.language = language; - } - public @Nullable String getConnectionType() { return connectionType; } @@ -477,7 +460,6 @@ public boolean equals(Object o) { && Objects.equals(screenDpi, device.screenDpi) && Objects.equals(bootTime, device.bootTime) && Objects.equals(id, device.id) - && Objects.equals(language, device.language) && Objects.equals(locale, device.locale) && Objects.equals(connectionType, device.connectionType) && Objects.equals(batteryTemperature, device.batteryTemperature) @@ -516,7 +498,6 @@ public int hashCode() { bootTime, timezone, id, - language, locale, connectionType, batteryTemperature, @@ -580,7 +561,6 @@ public static final class JsonKeys { public static final String BOOT_TIME = "boot_time"; public static final String TIMEZONE = "timezone"; public static final String ID = "id"; - public static final String LANGUAGE = "language"; public static final String CONNECTION_TYPE = "connection_type"; public static final String BATTERY_TEMPERATURE = "battery_temperature"; public static final String LOCALE = "locale"; @@ -674,9 +654,6 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (id != null) { writer.name(JsonKeys.ID).value(id); } - if (language != null) { - writer.name(JsonKeys.LANGUAGE).value(language); - } if (connectionType != null) { writer.name(JsonKeys.CONNECTION_TYPE).value(connectionType); } @@ -822,9 +799,6 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.ID: device.id = reader.nextStringOrNull(); break; - case JsonKeys.LANGUAGE: - device.language = reader.nextStringOrNull(); - break; case JsonKeys.CONNECTION_TYPE: device.connectionType = reader.nextStringOrNull(); break; diff --git a/sentry/src/main/java/io/sentry/protocol/Mechanism.java b/sentry/src/main/java/io/sentry/protocol/Mechanism.java index fac8808f2db..8945f6b3d00 100644 --- a/sentry/src/main/java/io/sentry/protocol/Mechanism.java +++ b/sentry/src/main/java/io/sentry/protocol/Mechanism.java @@ -67,6 +67,18 @@ public final class Mechanism implements JsonUnknown, JsonSerializable { * for grouping or display purposes. */ private @Nullable Boolean synthetic; + /** + * Exception ID. Used. e.g. for exception groups to build a hierarchy. This is referenced as + * parent by child exceptions which for Java SDK means Throwable.getSuppressed(). + */ + private @Nullable Integer exceptionId; + /** Parent exception ID. Used e.g. for exception groups to build a hierarchy. */ + private @Nullable Integer parentId; + /** + * Whether this is a group of exceptions. For Java SDK this means there were suppressed + * exceptions. + */ + private @Nullable Boolean exceptionGroup; @SuppressWarnings("unused") private @Nullable Map unknown; @@ -140,6 +152,30 @@ public void setSynthetic(final @Nullable Boolean synthetic) { this.synthetic = synthetic; } + public @Nullable Integer getExceptionId() { + return exceptionId; + } + + public void setExceptionId(final @Nullable Integer exceptionId) { + this.exceptionId = exceptionId; + } + + public @Nullable Integer getParentId() { + return parentId; + } + + public void setParentId(final @Nullable Integer parentId) { + this.parentId = parentId; + } + + public @Nullable Boolean isExceptionGroup() { + return exceptionGroup; + } + + public void setExceptionGroup(final @Nullable Boolean exceptionGroup) { + this.exceptionGroup = exceptionGroup; + } + // JsonKeys public static final class JsonKeys { @@ -150,6 +186,9 @@ public static final class JsonKeys { public static final String META = "meta"; public static final String DATA = "data"; public static final String SYNTHETIC = "synthetic"; + public static final String EXCEPTION_ID = "exception_id"; + public static final String PARENT_ID = "parent_id"; + public static final String IS_EXCEPTION_GROUP = "is_exception_group"; } // JsonUnknown @@ -191,6 +230,15 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (synthetic != null) { writer.name(JsonKeys.SYNTHETIC).value(synthetic); } + if (exceptionId != null) { + writer.name(JsonKeys.EXCEPTION_ID).value(logger, exceptionId); + } + if (parentId != null) { + writer.name(JsonKeys.PARENT_ID).value(logger, parentId); + } + if (exceptionGroup != null) { + writer.name(JsonKeys.IS_EXCEPTION_GROUP).value(exceptionGroup); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -238,6 +286,15 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.SYNTHETIC: mechanism.synthetic = reader.nextBooleanOrNull(); break; + case JsonKeys.EXCEPTION_ID: + mechanism.exceptionId = reader.nextIntegerOrNull(); + break; + case JsonKeys.PARENT_ID: + mechanism.parentId = reader.nextIntegerOrNull(); + break; + case JsonKeys.IS_EXCEPTION_GROUP: + mechanism.exceptionGroup = reader.nextBooleanOrNull(); + break; default: if (unknown == null) { unknown = new HashMap<>(); diff --git a/sentry/src/main/java/io/sentry/protocol/MetricSummary.java b/sentry/src/main/java/io/sentry/protocol/MetricSummary.java index f4a8b6de53a..8b137891791 100644 --- a/sentry/src/main/java/io/sentry/protocol/MetricSummary.java +++ b/sentry/src/main/java/io/sentry/protocol/MetricSummary.java @@ -1,163 +1 @@ -package io.sentry.protocol; -import io.sentry.ILogger; -import io.sentry.JsonDeserializer; -import io.sentry.JsonSerializable; -import io.sentry.JsonUnknown; -import io.sentry.ObjectReader; -import io.sentry.ObjectWriter; -import io.sentry.util.CollectionUtils; -import io.sentry.vendor.gson.stream.JsonToken; -import java.io.IOException; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** A summary for a metric, usually attached to spans. */ -public final class MetricSummary implements JsonUnknown, JsonSerializable { - - public static final class JsonKeys { - public static final String TAGS = "tags"; - public static final String MIN = "min"; - public static final String MAX = "max"; - public static final String COUNT = "count"; - public static final String SUM = "sum"; - } - - private double min; - private double max; - private double sum; - private int count; - private @Nullable Map tags; - private @Nullable Map unknown; - - public MetricSummary() {} - - public MetricSummary( - final double min, - final double max, - final double sum, - final int count, - final @Nullable Map tags) { - - this.tags = tags; - this.min = min; - this.max = max; - this.count = count; - this.sum = sum; - - this.unknown = null; - } - - public void setTags(final @Nullable Map tags) { - this.tags = tags; - } - - public void setMin(final double min) { - this.min = min; - } - - public void setMax(final double max) { - this.max = max; - } - - public void setCount(final int count) { - this.count = count; - } - - public void setSum(final double sum) { - this.sum = sum; - } - - public double getMin() { - return min; - } - - public double getMax() { - return max; - } - - public double getSum() { - return sum; - } - - public int getCount() { - return count; - } - - @Nullable - public Map getTags() { - return tags; - } - - @Override - public @Nullable Map getUnknown() { - return unknown; - } - - @Override - public void setUnknown(final @Nullable Map unknown) { - this.unknown = unknown; - } - - @Override - public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) - throws IOException { - writer.beginObject(); - writer.name(JsonKeys.MIN).value(min); - writer.name(JsonKeys.MAX).value(max); - writer.name(JsonKeys.SUM).value(sum); - writer.name(JsonKeys.COUNT).value(count); - if (tags != null) { - writer.name(JsonKeys.TAGS); - writer.value(logger, tags); - } - writer.endObject(); - } - - @SuppressWarnings("unchecked") - public static final class Deserializer implements JsonDeserializer { - - @Override - public @NotNull MetricSummary deserialize( - final @NotNull ObjectReader reader, final @NotNull ILogger logger) throws Exception { - - final @NotNull MetricSummary summary = new MetricSummary(); - @Nullable Map unknown = null; - - reader.beginObject(); - while (reader.peek() == JsonToken.NAME) { - final @NotNull String nextName = reader.nextName(); - switch (nextName) { - case JsonKeys.TAGS: - summary.tags = - CollectionUtils.newConcurrentHashMap( - (Map) reader.nextObjectOrNull()); - break; - case JsonKeys.MIN: - summary.setMin(reader.nextDouble()); - break; - case JsonKeys.MAX: - summary.setMax(reader.nextDouble()); - break; - case JsonKeys.SUM: - summary.setSum(reader.nextDouble()); - break; - case JsonKeys.COUNT: - summary.setCount(reader.nextInt()); - break; - default: - if (unknown == null) { - unknown = new ConcurrentHashMap<>(); - } - reader.nextUnknown(logger, unknown, nextName); - break; - } - } - summary.setUnknown(unknown); - reader.endObject(); - return summary; - } - } -} diff --git a/sentry/src/main/java/io/sentry/protocol/SdkVersion.java b/sentry/src/main/java/io/sentry/protocol/SdkVersion.java index aa997910be7..50af4f6a4b7 100644 --- a/sentry/src/main/java/io/sentry/protocol/SdkVersion.java +++ b/sentry/src/main/java/io/sentry/protocol/SdkVersion.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -98,40 +97,12 @@ public void addIntegration(final @NotNull String integration) { SentryIntegrationPackageStorage.getInstance().addIntegration(integration); } - /** - * Gets installed Sentry packages as list - * - * @deprecated use {@link SdkVersion#getPackageSet()} ()} - */ - @Deprecated - public @Nullable List getPackages() { - final Set packages = - deserializedPackages != null - ? deserializedPackages - : SentryIntegrationPackageStorage.getInstance().getPackages(); - return new CopyOnWriteArrayList<>(packages); - } - public @NotNull Set getPackageSet() { return deserializedPackages != null ? deserializedPackages : SentryIntegrationPackageStorage.getInstance().getPackages(); } - /** - * Gets installed Sentry integration names as list - * - * @deprecated use {@link SdkVersion#getIntegrationSet()} - */ - @Deprecated - public @Nullable List getIntegrations() { - final Set integrations = - deserializedIntegrations != null - ? deserializedIntegrations - : SentryIntegrationPackageStorage.getInstance().getIntegrations(); - return new CopyOnWriteArrayList<>(integrations); - } - public @NotNull Set getIntegrationSet() { return deserializedIntegrations != null ? deserializedIntegrations diff --git a/sentry/src/main/java/io/sentry/protocol/SentryId.java b/sentry/src/main/java/io/sentry/protocol/SentryId.java index 109655fdf2b..a5bd7980c3f 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryId.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryId.java @@ -5,35 +5,53 @@ import io.sentry.JsonSerializable; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryUUID; +import io.sentry.util.LazyEvaluator; import io.sentry.util.StringUtils; +import io.sentry.util.UUIDStringUtils; import java.io.IOException; import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class SentryId implements JsonSerializable { - private final @NotNull UUID uuid; - public static final SentryId EMPTY_ID = new SentryId(new UUID(0, 0)); + public static final SentryId EMPTY_ID = + new SentryId(StringUtils.PROPER_NIL_UUID.replace("-", "")); + + private final @NotNull LazyEvaluator lazyStringValue; public SentryId() { this((UUID) null); } public SentryId(@Nullable UUID uuid) { - if (uuid == null) { - uuid = UUID.randomUUID(); + if (uuid != null) { + this.lazyStringValue = + new LazyEvaluator<>(() -> normalize(UUIDStringUtils.toSentryIdString(uuid))); + } else { + this.lazyStringValue = new LazyEvaluator<>(SentryUUID::generateSentryId); } - this.uuid = uuid; } public SentryId(final @NotNull String sentryIdString) { - this.uuid = fromStringSentryId(StringUtils.normalizeUUID(sentryIdString)); + final @NotNull String normalized = StringUtils.normalizeUUID(sentryIdString); + if (normalized.length() != 32 && normalized.length() != 36) { + throw new IllegalArgumentException( + "String representation of SentryId has either 32 (UUID no dashes) " + + "or 36 characters long (completed UUID). Received: " + + sentryIdString); + } + if (normalized.length() == 36) { + this.lazyStringValue = new LazyEvaluator<>(() -> normalize(normalized)); + } else { + this.lazyStringValue = new LazyEvaluator<>(() -> normalized); + } } @Override public String toString() { - return StringUtils.normalizeUUID(uuid.toString()).replace("-", ""); + return lazyStringValue.getValue(); } @Override @@ -41,33 +59,16 @@ public boolean equals(final @Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SentryId sentryId = (SentryId) o; - return uuid.compareTo(sentryId.uuid) == 0; + return lazyStringValue.getValue().equals(sentryId.lazyStringValue.getValue()); } @Override public int hashCode() { - return uuid.hashCode(); + return lazyStringValue.getValue().hashCode(); } - private @NotNull UUID fromStringSentryId(@NotNull String sentryIdString) { - if (sentryIdString.length() == 32) { - // expected format, SentryId is a UUID without dashes - sentryIdString = - new StringBuilder(sentryIdString) - .insert(8, "-") - .insert(13, "-") - .insert(18, "-") - .insert(23, "-") - .toString(); - } - if (sentryIdString.length() != 36) { - throw new IllegalArgumentException( - "String representation of SentryId has either 32 (UUID no dashes) " - + "or 36 characters long (completed UUID). Received: " - + sentryIdString); - } - - return UUID.fromString(sentryIdString); + private @NotNull String normalize(@NotNull String uuidString) { + return StringUtils.normalizeUUID(uuidString).replace("-", ""); } // JsonSerializable diff --git a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java index f4c8d20efa1..8ab31beb835 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java +++ b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java @@ -11,7 +11,6 @@ import io.sentry.Span; import io.sentry.SpanId; import io.sentry.SpanStatus; -import io.sentry.metrics.LocalMetricsAggregator; import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; @@ -20,7 +19,6 @@ import java.math.RoundingMode; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus; @@ -43,7 +41,6 @@ public final class SentrySpan implements JsonUnknown, JsonSerializable { private @Nullable Map data; private final @NotNull Map measurements; - private final @Nullable Map> metricsSummaries; @SuppressWarnings("unused") private @Nullable Map unknown; @@ -76,13 +73,6 @@ public SentrySpan(final @NotNull Span span, final @Nullable Map // we lose precision here, from potential nanosecond precision down to 10 microsecond precision this.startTimestamp = DateUtils.nanosToSeconds(span.getStartDate().nanoTimestamp()); this.data = data; - - final @Nullable LocalMetricsAggregator localAggregator = span.getLocalMetricsAggregator(); - if (localAggregator != null) { - this.metricsSummaries = localAggregator.getSummaries(); - } else { - this.metricsSummaries = null; - } } @ApiStatus.Internal @@ -98,7 +88,6 @@ public SentrySpan( @Nullable String origin, @NotNull Map tags, @NotNull Map measurements, - @Nullable Map> metricSummaries, @Nullable Map data) { this.startTimestamp = startTimestamp; this.timestamp = timestamp; @@ -111,7 +100,6 @@ public SentrySpan( this.origin = origin; this.tags = tags; this.measurements = measurements; - this.metricsSummaries = metricSummaries; this.data = data; } @@ -171,10 +159,6 @@ public void setData(final @Nullable Map data) { return measurements; } - public @Nullable Map> getMetricsSummaries() { - return metricsSummaries; - } - // JsonSerializable public static final class JsonKeys { @@ -189,7 +173,6 @@ public static final class JsonKeys { public static final String ORIGIN = "origin"; public static final String TAGS = "tags"; public static final String MEASUREMENTS = "measurements"; - public static final String METRICS_SUMMARY = "_metrics_summary"; public static final String DATA = "data"; } @@ -225,9 +208,6 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (!measurements.isEmpty()) { writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); } - if (metricsSummaries != null && !metricsSummaries.isEmpty()) { - writer.name(JsonKeys.METRICS_SUMMARY).value(logger, metricsSummaries); - } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -272,7 +252,6 @@ public static final class Deserializer implements JsonDeserializer { String origin = null; Map tags = null; Map measurements = null; - Map> metricSummaries = null; Map data = null; Map unknown = null; @@ -325,9 +304,6 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.MEASUREMENTS: measurements = reader.nextMapOrNull(logger, new MeasurementValue.Deserializer()); break; - case JsonKeys.METRICS_SUMMARY: - metricSummaries = reader.nextMapOfListOrNull(logger, new MetricSummary.Deserializer()); - break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); @@ -367,7 +343,6 @@ public static final class Deserializer implements JsonDeserializer { origin, tags, measurements, - metricSummaries, data); sentrySpan.setUnknown(unknown); reader.endObject(); diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index 3bc42e42084..684001843a8 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -13,7 +13,6 @@ import io.sentry.SpanContext; import io.sentry.SpanStatus; import io.sentry.TracesSamplingDecision; -import io.sentry.metrics.LocalMetricsAggregator; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; @@ -29,7 +28,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -@ApiStatus.Internal public final class SentryTransaction extends SentryBaseEvent implements JsonUnknown, JsonSerializable { /** The transaction name. */ @@ -51,8 +49,6 @@ public final class SentryTransaction extends SentryBaseEvent private @NotNull final Map measurements = new HashMap<>(); - private @Nullable Map> metricSummaries; - private @NotNull TransactionInfo transactionInfo; private @Nullable Map unknown; @@ -80,8 +76,9 @@ public SentryTransaction(final @NotNull SentryTracer sentryTracer) { contexts.putAll(sentryTracer.getContexts()); final SpanContext tracerContext = sentryTracer.getSpanContext(); + Map data = sentryTracer.getData(); // tags must be placed on the root of the transaction instead of contexts.trace.tags - contexts.setTrace( + final SpanContext tracerContextToSend = new SpanContext( tracerContext.getTraceId(), tracerContext.getSpanId(), @@ -90,27 +87,21 @@ public SentryTransaction(final @NotNull SentryTracer sentryTracer) { tracerContext.getDescription(), tracerContext.getSamplingDecision(), tracerContext.getStatus(), - tracerContext.getOrigin())); + tracerContext.getOrigin()); + for (final Map.Entry tag : tracerContext.getTags().entrySet()) { this.setTag(tag.getKey(), tag.getValue()); } - final Map data = sentryTracer.getData(); if (data != null) { for (final Map.Entry tag : data.entrySet()) { - this.setExtra(tag.getKey(), tag.getValue()); + tracerContextToSend.setData(tag.getKey(), tag.getValue()); } } - this.transactionInfo = new TransactionInfo(sentryTracer.getTransactionNameSource().apiName()); + contexts.setTrace(tracerContextToSend); - final @Nullable LocalMetricsAggregator localAggregator = - sentryTracer.getLocalMetricsAggregator(); - if (localAggregator != null) { - this.metricSummaries = localAggregator.getSummaries(); - } else { - this.metricSummaries = null; - } + this.transactionInfo = new TransactionInfo(sentryTracer.getTransactionNameSource().apiName()); } @ApiStatus.Internal @@ -120,7 +111,6 @@ public SentryTransaction( @Nullable Double timestamp, @NotNull List spans, @NotNull final Map measurements, - @Nullable Map> metricsSummaries, @NotNull final TransactionInfo transactionInfo) { this.transaction = transaction; this.startTimestamp = startTimestamp; @@ -131,7 +121,6 @@ public SentryTransaction( this.measurements.putAll(span.getMeasurements()); } this.transactionInfo = transactionInfo; - this.metricSummaries = metricsSummaries; } public @NotNull List getSpans() { @@ -185,14 +174,6 @@ public boolean isSampled() { return measurements; } - public @Nullable Map> getMetricSummaries() { - return metricSummaries; - } - - public void setMetricSummaries(final @Nullable Map> metricSummaries) { - this.metricSummaries = metricSummaries; - } - // JsonSerializable public static final class JsonKeys { @@ -202,7 +183,6 @@ public static final class JsonKeys { public static final String SPANS = "spans"; public static final String TYPE = "type"; public static final String MEASUREMENTS = "measurements"; - public static final String METRICS_SUMMARY = "_metrics_summary"; public static final String TRANSACTION_INFO = "transaction_info"; } @@ -224,9 +204,6 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (!measurements.isEmpty()) { writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); } - if (metricSummaries != null && !metricSummaries.isEmpty()) { - writer.name(JsonKeys.METRICS_SUMMARY).value(logger, metricSummaries); - } writer.name(JsonKeys.TRANSACTION_INFO).value(logger, transactionInfo); new SentryBaseEvent.Serializer().serialize(this, writer, logger); if (unknown != null) { @@ -270,7 +247,6 @@ public static final class Deserializer implements JsonDeserializer(), new HashMap<>(), - null, new TransactionInfo(TransactionNameSource.CUSTOM.apiName())); Map unknown = null; @@ -325,10 +301,6 @@ public static final class Deserializer implements JsonDeserializer data; /** unknown fields, only internal usage. */ @@ -65,7 +56,6 @@ public User(final @NotNull User user) { this.username = user.username; this.id = user.id; this.ipAddress = user.ipAddress; - this.segment = user.segment; this.name = user.name; this.geo = user.geo; this.data = CollectionUtils.newConcurrentHashMap(user.data); @@ -99,9 +89,6 @@ public static User fromMap(@NotNull Map map, @NotNull SentryOpti case JsonKeys.USERNAME: user.username = (value instanceof String) ? (String) value : null; break; - case JsonKeys.SEGMENT: - user.segment = (value instanceof String) ? (String) value : null; - break; case JsonKeys.IP_ADDRESS: user.ipAddress = (value instanceof String) ? (String) value : null; break; @@ -140,24 +127,6 @@ public static User fromMap(@NotNull Map map, @NotNull SentryOpti user.data = userData; } break; - case JsonKeys.OTHER: - final Map other = - (value instanceof Map) ? (Map) value : null; - // restore `other` from legacy JSON - if (other != null && (user.data == null || user.data.isEmpty())) { - final ConcurrentHashMap userData = new ConcurrentHashMap<>(); - for (Map.Entry otherEntry : other.entrySet()) { - if (otherEntry.getKey() instanceof String && otherEntry.getValue() != null) { - userData.put((String) otherEntry.getKey(), otherEntry.getValue().toString()); - } else { - options - .getLogger() - .log(SentryLevel.WARNING, "Invalid key or null value in other map."); - } - } - user.data = userData; - } - break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); @@ -224,28 +193,6 @@ public void setUsername(final @Nullable String username) { this.username = username; } - /** - * Gets the segment of the user. - * - * @return the user segment. - * @deprecated has no effect and will be removed in the next major update. - */ - @Deprecated - public @Nullable String getSegment() { - return segment; - } - - /** - * Sets the segment of the user. - * - * @param segment the segment. - * @deprecated has no effect and will be removed in the next major update. - */ - @Deprecated - public void setSegment(final @Nullable String segment) { - this.segment = segment; - } - /** * Gets the IP address of the user. * @@ -264,30 +211,6 @@ public void setIpAddress(final @Nullable String ipAddress) { this.ipAddress = ipAddress; } - /** - * Gets other user related data. - * - * @deprecated use {{@link User#getData()}} instead - * @return the other user data. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public @Nullable Map getOthers() { - return getData(); - } - - /** - * Sets other user related data. - * - * @deprecated use {{@link User#setData(Map)}} instead - * @param other the other user related data. - */ - @Deprecated - @SuppressWarnings("InlineMeSuggester") - public void setOthers(final @Nullable Map other) { - this.setData(other); - } - /** * Get human readable name. * @@ -350,13 +273,12 @@ public boolean equals(Object o) { return Objects.equals(email, user.email) && Objects.equals(id, user.id) && Objects.equals(username, user.username) - && Objects.equals(segment, user.segment) && Objects.equals(ipAddress, user.ipAddress); } @Override public int hashCode() { - return Objects.hash(email, id, username, segment, ipAddress); + return Objects.hash(email, id, username, ipAddress); } // region json @@ -376,11 +298,9 @@ public static final class JsonKeys { public static final String EMAIL = "email"; public static final String ID = "id"; public static final String USERNAME = "username"; - public static final String SEGMENT = "segment"; public static final String IP_ADDRESS = "ip_address"; public static final String NAME = "name"; public static final String GEO = "geo"; - public static final String OTHER = "other"; public static final String DATA = "data"; } @@ -397,9 +317,6 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (username != null) { writer.name(JsonKeys.USERNAME).value(username); } - if (segment != null) { - writer.name(JsonKeys.SEGMENT).value(segment); - } if (ipAddress != null) { writer.name(JsonKeys.IP_ADDRESS).value(ipAddress); } @@ -443,9 +360,6 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.USERNAME: user.username = reader.nextStringOrNull(); break; - case JsonKeys.SEGMENT: - user.segment = reader.nextStringOrNull(); - break; case JsonKeys.IP_ADDRESS: user.ipAddress = reader.nextStringOrNull(); break; @@ -460,14 +374,6 @@ public static final class Deserializer implements JsonDeserializer { CollectionUtils.newConcurrentHashMap( (Map) reader.nextObjectOrNull()); break; - case JsonKeys.OTHER: - // restore `other` from legacy JSON - if (user.data == null || user.data.isEmpty()) { - user.data = - CollectionUtils.newConcurrentHashMap( - (Map) reader.nextObjectOrNull()); - } - break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 191e8cbe7ef..3fc8293bf1f 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -5,14 +5,16 @@ import io.sentry.DataCategory; import io.sentry.Hint; +import io.sentry.ISentryLifecycleToken; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.clientreport.DiscardReason; +import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; -import io.sentry.util.CollectionUtils; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.HintUtils; import io.sentry.util.StringUtils; import java.io.Closeable; @@ -39,7 +41,7 @@ public final class RateLimiter implements Closeable { new ConcurrentHashMap<>(); private final @NotNull List rateLimitObservers = new CopyOnWriteArrayList<>(); private @Nullable Timer timer = null; - private final @NotNull Object timerLock = new Object(); + private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); public RateLimiter( final @NotNull ICurrentDateProvider currentDateProvider, @@ -144,9 +146,16 @@ public boolean isAnyRateLimitActive() { * @param hint the Hints * @param retry if event should be retried or not */ - private static void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { + private void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { HintUtils.runIfHasType(hint, SubmissionResult.class, result -> result.setResult(false)); HintUtils.runIfHasType(hint, Retryable.class, retryable -> retryable.setRetry(retry)); + HintUtils.runIfHasType( + hint, + DiskFlushNotification.class, + (diskFlushNotification) -> { + diskFlushNotification.markFlushed(); + options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired due to rate limit"); + }); } /** @@ -177,10 +186,6 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Attachment; case "profile": return DataCategory.Profile; - // The envelope item type used for metrics is statsd, whereas the client report category is - // metric_bucket - case "statsd": - return DataCategory.MetricBucket; case "transaction": return DataCategory.Transaction; case "check_in": @@ -215,7 +220,7 @@ public void updateRetryAfterLimits( // These can be ignored by the SDK. // final String scope = rateLimit.length > 2 ? rateLimit[2] : null; // final String reasonCode = rateLimit.length > 3 ? rateLimit[3] : null; - final @Nullable String limitNamespaces = rateLimit.length > 4 ? rateLimit[4] : null; + // final @Nullable String limitNamespaces = rateLimit.length > 4 ? rateLimit[4] : null; if (rateLimit.length > 0) { final String retryAfter = rateLimit[0]; @@ -247,17 +252,6 @@ public void updateRetryAfterLimits( if (DataCategory.Unknown.equals(dataCategory)) { continue; } - // SDK doesn't support namespaces, yet. Namespaces can be returned by relay in case - // of metric_bucket items. If the namespaces are empty or contain "custom" we apply - // the rate limit to all metrics, otherwise to none. - if (DataCategory.MetricBucket.equals(dataCategory) - && limitNamespaces != null - && !limitNamespaces.equals("")) { - final String[] namespaces = limitNamespaces.split(";", -1); - if (namespaces.length > 0 && !CollectionUtils.contains(namespaces, "custom")) { - continue; - } - } applyRetryAfterOnlyIfLonger(dataCategory, date); } @@ -293,7 +287,7 @@ private void applyRetryAfterOnlyIfLonger( notifyRateLimitObservers(); - synchronized (timerLock) { + try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { if (timer == null) { timer = new Timer(true); } @@ -345,7 +339,7 @@ public void removeRateLimitObserver(@NotNull final IRateLimitObserver observer) @Override public void close() throws IOException { - synchronized (timerLock) { + try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { if (timer != null) { timer.cancel(); timer = null; diff --git a/sentry/src/main/java/io/sentry/util/AutoClosableReentrantLock.java b/sentry/src/main/java/io/sentry/util/AutoClosableReentrantLock.java new file mode 100644 index 00000000000..2a95a58b5fe --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/AutoClosableReentrantLock.java @@ -0,0 +1,29 @@ +package io.sentry.util; + +import io.sentry.ISentryLifecycleToken; +import java.util.concurrent.locks.ReentrantLock; +import org.jetbrains.annotations.NotNull; + +public final class AutoClosableReentrantLock extends ReentrantLock { + + private static final long serialVersionUID = -3283069816958445549L; + + public ISentryLifecycleToken acquire() { + lock(); + return new AutoClosableReentrantLockLifecycleToken(this); + } + + static final class AutoClosableReentrantLockLifecycleToken implements ISentryLifecycleToken { + + private final @NotNull ReentrantLock lock; + + AutoClosableReentrantLockLifecycleToken(final @NotNull ReentrantLock lock) { + this.lock = lock; + } + + @Override + public void close() { + lock.unlock(); + } + } +} diff --git a/sentry/src/main/java/io/sentry/util/CheckInUtils.java b/sentry/src/main/java/io/sentry/util/CheckInUtils.java index e15603adaf5..7b44fffbc35 100644 --- a/sentry/src/main/java/io/sentry/util/CheckInUtils.java +++ b/sentry/src/main/java/io/sentry/util/CheckInUtils.java @@ -3,7 +3,9 @@ import io.sentry.CheckIn; import io.sentry.CheckInStatus; import io.sentry.DateUtils; -import io.sentry.IHub; +import io.sentry.FilterString; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; import io.sentry.MonitorConfig; import io.sentry.Sentry; import io.sentry.protocol.SentryId; @@ -21,41 +23,84 @@ public final class CheckInUtils { * * @param monitorSlug - the slug of the monitor * @param monitorConfig - configuration of the monitor, can be used for upserting schedule + * @param environment - the name of the environment * @param callable - the {@link Callable} to be called * @return the return value of the {@link Callable} * @param - the result type of the {@link Callable} */ public static U withCheckIn( final @NotNull String monitorSlug, + final @Nullable String environment, final @Nullable MonitorConfig monitorConfig, final @NotNull Callable callable) throws Exception { - final @NotNull IHub hub = Sentry.getCurrentHub(); - final long startTime = System.currentTimeMillis(); - boolean didError = false; + try (final @NotNull ISentryLifecycleToken ignored = + Sentry.forkedScopes("CheckInUtils").makeCurrent()) { + final @NotNull IScopes scopes = Sentry.getCurrentScopes(); + final long startTime = System.currentTimeMillis(); + boolean didError = false; - hub.pushScope(); - TracingUtils.startNewTrace(hub); + TracingUtils.startNewTrace(scopes); - CheckIn inProgressCheckIn = new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS); - if (monitorConfig != null) { - inProgressCheckIn.setMonitorConfig(monitorConfig); - } - @Nullable SentryId checkInId = hub.captureCheckIn(inProgressCheckIn); - try { - return callable.call(); - } catch (Throwable t) { - didError = true; - throw t; - } finally { - final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; - CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); - checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); - hub.captureCheckIn(checkIn); - hub.popScope(); + CheckIn inProgressCheckIn = new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS); + if (monitorConfig != null) { + inProgressCheckIn.setMonitorConfig(monitorConfig); + } + if (environment != null) { + inProgressCheckIn.setEnvironment(environment); + } + @Nullable SentryId checkInId = scopes.captureCheckIn(inProgressCheckIn); + try { + return callable.call(); + } catch (Throwable t) { + didError = true; + throw t; + } finally { + final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; + CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); + if (environment != null) { + checkIn.setEnvironment(environment); + } + checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); + scopes.captureCheckIn(checkIn); + } } } + /** + * Helper method to send monitor check-ins for a {@link Callable} + * + * @param monitorSlug - the slug of the monitor + * @param monitorConfig - configuration of the monitor, can be used for upserting schedule + * @param callable - the {@link Callable} to be called + * @return the return value of the {@link Callable} + * @param - the result type of the {@link Callable} + */ + public static U withCheckIn( + final @NotNull String monitorSlug, + final @Nullable MonitorConfig monitorConfig, + final @NotNull Callable callable) + throws Exception { + return withCheckIn(monitorSlug, null, monitorConfig, callable); + } + + /** + * Helper method to send monitor check-ins for a {@link Callable} + * + * @param monitorSlug - the slug of the monitor + * @param environment - the name of the environment + * @param callable - the {@link Callable} to be called + * @return the return value of the {@link Callable} + * @param - the result type of the {@link Callable} + */ + public static U withCheckIn( + final @NotNull String monitorSlug, + final @Nullable String environment, + final @NotNull Callable callable) + throws Exception { + return withCheckIn(monitorSlug, environment, null, callable); + } + /** * Helper method to send monitor check-ins for a {@link Callable} * @@ -66,24 +111,26 @@ public static U withCheckIn( */ public static U withCheckIn( final @NotNull String monitorSlug, final @NotNull Callable callable) throws Exception { - return withCheckIn(monitorSlug, null, callable); + return withCheckIn(monitorSlug, null, null, callable); } /** Checks if a check-in for a monitor (CRON) has been ignored. */ @ApiStatus.Internal public static boolean isIgnored( - final @Nullable List ignoredSlugs, final @NotNull String slug) { + final @Nullable List ignoredSlugs, final @NotNull String slug) { if (ignoredSlugs == null || ignoredSlugs.isEmpty()) { return false; } - for (final String ignoredSlug : ignoredSlugs) { - if (ignoredSlug.equalsIgnoreCase(slug)) { + for (final FilterString ignoredSlug : ignoredSlugs) { + if (ignoredSlug.getFilterString().equalsIgnoreCase(slug)) { return true; } + } + for (final FilterString ignoredSlug : ignoredSlugs) { try { - if (slug.matches(ignoredSlug)) { + if (ignoredSlug.matches(slug)) { return true; } } catch (Throwable t) { diff --git a/sentry/src/main/java/io/sentry/util/EventProcessorUtils.java b/sentry/src/main/java/io/sentry/util/EventProcessorUtils.java new file mode 100644 index 00000000000..8fb73982c0f --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/EventProcessorUtils.java @@ -0,0 +1,24 @@ +package io.sentry.util; + +import io.sentry.EventProcessor; +import io.sentry.internal.eventprocessor.EventProcessorAndOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.jetbrains.annotations.Nullable; + +public final class EventProcessorUtils { + + public static List unwrap( + final @Nullable List orderedEventProcessor) { + final List eventProcessors = new ArrayList<>(); + + if (orderedEventProcessor != null) { + for (EventProcessorAndOrder eventProcessorAndOrder : orderedEventProcessor) { + eventProcessors.add(eventProcessorAndOrder.getEventProcessor()); + } + } + + return new CopyOnWriteArrayList<>(eventProcessors); + } +} diff --git a/sentry/src/main/java/io/sentry/util/HttpUtils.java b/sentry/src/main/java/io/sentry/util/HttpUtils.java index 424bcbc65d6..399ba7013fe 100644 --- a/sentry/src/main/java/io/sentry/util/HttpUtils.java +++ b/sentry/src/main/java/io/sentry/util/HttpUtils.java @@ -2,6 +2,7 @@ import static io.sentry.util.UrlUtils.SENSITIVE_DATA_SUBSTITUTE; +import io.sentry.HttpStatusCodeRange; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -42,6 +43,12 @@ public final class HttpUtils { "CSRFTOKEN", "XSRF-TOKEN"); + private static final HttpStatusCodeRange CLIENT_ERROR_STATUS_CODES = + new HttpStatusCodeRange(400, 499); + + private static final HttpStatusCodeRange SEVER_ERROR_STATUS_CODES = + new HttpStatusCodeRange(500, 599); + public static boolean containsSensitiveHeader(final @NotNull String header) { return SENSITIVE_HEADERS.contains(header.toUpperCase(Locale.ROOT)); } @@ -130,4 +137,12 @@ public static boolean isSecurityCookie( return false; } + + public static boolean isHttpClientError(final int statusCode) { + return CLIENT_ERROR_STATUS_CODES.isInRange(statusCode); + } + + public static boolean isHttpServerError(final int statusCode) { + return SEVER_ERROR_STATUS_CODES.isInRange(statusCode); + } } diff --git a/sentry/src/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java new file mode 100644 index 00000000000..f651383a788 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/InitUtil.java @@ -0,0 +1,28 @@ +package io.sentry.util; + +import io.sentry.SentryOptions; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class InitUtil { + public static boolean shouldInit( + final @Nullable SentryOptions previousOptions, + final @NotNull SentryOptions newOptions, + final boolean isEnabled) { + if (!isEnabled) { + return true; + } + + if (previousOptions == null) { + return true; + } + + if (newOptions.isForceInit()) { + return true; + } + + return previousOptions.getInitPriority().ordinal() <= newOptions.getInitPriority().ordinal(); + } +} diff --git a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java index 8b3a6cce53a..82c2b866d2c 100644 --- a/sentry/src/main/java/io/sentry/util/LazyEvaluator.java +++ b/sentry/src/main/java/io/sentry/util/LazyEvaluator.java @@ -1,5 +1,6 @@ package io.sentry.util; +import io.sentry.ISentryLifecycleToken; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -13,6 +14,7 @@ public final class LazyEvaluator { private volatile @Nullable T value = null; private final @NotNull Evaluator evaluator; + private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); /** * Class that evaluates a function lazily. It means the evaluator function is called only when @@ -32,7 +34,7 @@ public LazyEvaluator(final @NotNull Evaluator evaluator) { */ public @NotNull T getValue() { if (value == null) { - synchronized (this) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { if (value == null) { value = evaluator.evaluate(); } @@ -44,7 +46,7 @@ public LazyEvaluator(final @NotNull Evaluator evaluator) { } public void setValue(final @Nullable T value) { - synchronized (this) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { this.value = value; } } @@ -54,7 +56,7 @@ public void setValue(final @Nullable T value) { * getValue() is called. */ public void resetValue() { - synchronized (this) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { this.value = null; } } diff --git a/sentry/src/main/java/io/sentry/util/LifecycleHelper.java b/sentry/src/main/java/io/sentry/util/LifecycleHelper.java new file mode 100644 index 00000000000..4a029f620cc --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/LifecycleHelper.java @@ -0,0 +1,15 @@ +package io.sentry.util; + +import io.sentry.ISentryLifecycleToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class LifecycleHelper { + + public static void close(final @Nullable Object tokenObject) { + if (tokenObject != null && tokenObject instanceof ISentryLifecycleToken) { + final @NotNull ISentryLifecycleToken token = (ISentryLifecycleToken) tokenObject; + token.close(); + } + } +} diff --git a/sentry/src/main/java/io/sentry/util/LoadClass.java b/sentry/src/main/java/io/sentry/util/LoadClass.java new file mode 100644 index 00000000000..11fef9ea01e --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/LoadClass.java @@ -0,0 +1,48 @@ +package io.sentry.util; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** An Adapter for making Class.forName testable */ +@Open +public class LoadClass { + + /** + * Try to load a class via reflection + * + * @param clazz the full class name + * @param logger an instance of ILogger + * @return a Class<?> if it's available, or null + */ + public @Nullable Class loadClass(final @NotNull String clazz, final @Nullable ILogger logger) { + try { + return Class.forName(clazz); + } catch (ClassNotFoundException e) { + if (logger != null) { + logger.log(SentryLevel.DEBUG, "Class not available:" + clazz, e); + } + } catch (UnsatisfiedLinkError e) { + if (logger != null) { + logger.log(SentryLevel.ERROR, "Failed to load (UnsatisfiedLinkError) " + clazz, e); + } + } catch (Throwable e) { + if (logger != null) { + logger.log(SentryLevel.ERROR, "Failed to initialize " + clazz, e); + } + } + return null; + } + + public boolean isClassAvailable(final @NotNull String clazz, final @Nullable ILogger logger) { + return loadClass(clazz, logger) != null; + } + + public boolean isClassAvailable( + final @NotNull String clazz, final @Nullable SentryOptions options) { + return isClassAvailable(clazz, options != null ? options.getLogger() : null); + } +} diff --git a/sentry/src/main/java/io/sentry/util/SpanUtils.java b/sentry/src/main/java/io/sentry/util/SpanUtils.java new file mode 100644 index 00000000000..ed846271713 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/SpanUtils.java @@ -0,0 +1,82 @@ +package io.sentry.util; + +import io.sentry.FilterString; +import io.sentry.SentryOpenTelemetryMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SpanUtils { + + /** + * A list of span origins that are ignored by default when using OpenTelemetry. + * + * @return a list of span origins to be ignored + */ + public static @NotNull List ignoredSpanOriginsForOpenTelemetry( + final @NotNull SentryOpenTelemetryMode mode) { + final @NotNull List origins = new ArrayList<>(); + + if (SentryOpenTelemetryMode.AGENT == mode || SentryOpenTelemetryMode.AGENTLESS_SPRING == mode) { + origins.add("auto.http.spring_jakarta.webmvc"); + origins.add("auto.http.spring.webmvc"); + origins.add("auto.spring_jakarta.webflux"); + origins.add("auto.spring.webflux"); + origins.add("auto.db.jdbc"); + origins.add("auto.http.spring_jakarta.webclient"); + origins.add("auto.http.spring.webclient"); + origins.add("auto.http.spring_jakarta.restclient"); + origins.add("auto.http.spring.restclient"); + origins.add("auto.http.spring_jakarta.resttemplate"); + origins.add("auto.http.spring.resttemplate"); + origins.add("auto.http.openfeign"); + } + + if (SentryOpenTelemetryMode.AGENT == mode) { + origins.add("auto.graphql.graphql"); + origins.add("auto.graphql.graphql22"); + } + + return origins; + } + + private static final Map ignoredSpanDecisionsCache = new ConcurrentHashMap<>(); + + /** Checks if a span origin has been ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @Nullable List ignoredOrigins, final @Nullable String origin) { + if (origin == null || ignoredOrigins == null || ignoredOrigins.isEmpty()) { + return false; + } + + if (ignoredSpanDecisionsCache.containsKey(origin)) { + return ignoredSpanDecisionsCache.get(origin); + } + + for (final FilterString ignoredOrigin : ignoredOrigins) { + if (ignoredOrigin.getFilterString().equalsIgnoreCase(origin)) { + ignoredSpanDecisionsCache.put(origin, true); + return true; + } + } + + for (final FilterString ignoredOrigin : ignoredOrigins) { + try { + if (ignoredOrigin.matches(origin)) { + ignoredSpanDecisionsCache.put(origin, true); + return true; + } + } catch (Throwable t) { + // ignore invalid regex + } + } + + ignoredSpanDecisionsCache.put(origin, false); + return false; + } +} diff --git a/sentry/src/main/java/io/sentry/util/StringUtils.java b/sentry/src/main/java/io/sentry/util/StringUtils.java index e4bf6185016..249940a638c 100644 --- a/sentry/src/main/java/io/sentry/util/StringUtils.java +++ b/sentry/src/main/java/io/sentry/util/StringUtils.java @@ -20,8 +20,8 @@ public final class StringUtils { private static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final String PROPER_NIL_UUID = "00000000-0000-0000-0000-000000000000"; private static final String CORRUPTED_NIL_UUID = "0000-0000"; - private static final String PROPER_NIL_UUID = "00000000-0000-0000-0000-000000000000"; private static final @NotNull Pattern PATTERN_WORD_SNAKE_CASE = Pattern.compile("[\\W_]+"); private StringUtils() {} diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index 2aeb613f2de..16655be634c 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -2,20 +2,22 @@ import io.sentry.Baggage; import io.sentry.BaggageHeader; -import io.sentry.IHub; +import io.sentry.FilterString; import io.sentry.IScope; +import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.PropagationContext; import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; import java.util.List; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class TracingUtils { - public static void startNewTrace(final @NotNull IHub hub) { - hub.configureScope( + public static void startNewTrace(final @NotNull IScopes scopes) { + scopes.configureScope( scope -> { scope.withPropagationContext( propagationContext -> { @@ -25,30 +27,30 @@ public static void startNewTrace(final @NotNull IHub hub) { } public static @Nullable TracingHeaders traceIfAllowed( - final @NotNull IHub hub, + final @NotNull IScopes scopes, final @NotNull String requestUrl, @Nullable List thirdPartyBaggageHeaders, final @Nullable ISpan span) { - final @NotNull SentryOptions sentryOptions = hub.getOptions(); + final @NotNull SentryOptions sentryOptions = scopes.getOptions(); if (sentryOptions.isTraceSampling() && shouldAttachTracingHeaders(requestUrl, sentryOptions)) { - return trace(hub, thirdPartyBaggageHeaders, span); + return trace(scopes, thirdPartyBaggageHeaders, span); } return null; } public static @Nullable TracingHeaders trace( - final @NotNull IHub hub, + final @NotNull IScopes scopes, @Nullable List thirdPartyBaggageHeaders, final @Nullable ISpan span) { - final @NotNull SentryOptions sentryOptions = hub.getOptions(); + final @NotNull SentryOptions sentryOptions = scopes.getOptions(); if (span != null && !span.isNoOp()) { return new TracingHeaders( span.toSentryTrace(), span.toBaggageHeader(thirdPartyBaggageHeaders)); } else { final @NotNull PropagationContextHolder returnValue = new PropagationContextHolder(); - hub.configureScope( + scopes.configureScope( (scope) -> { returnValue.propagationContext = maybeUpdateBaggage(scope, sentryOptions); }); @@ -64,7 +66,9 @@ public static void startNewTrace(final @NotNull IHub hub) { return new TracingHeaders( new SentryTraceHeader( - propagationContext.getTraceId(), propagationContext.getSpanId(), null), + propagationContext.getTraceId(), + propagationContext.getSpanId(), + propagationContext.isSampled()), baggageHeader); } @@ -116,4 +120,36 @@ public TracingHeaders( return baggageHeader; } } + + /** Checks if a transaction is to be ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @Nullable List ignoredTransactions, + final @Nullable String transactionName) { + if (transactionName == null) { + return false; + } + if (ignoredTransactions == null || ignoredTransactions.isEmpty()) { + return false; + } + + for (final FilterString ignoredTransaction : ignoredTransactions) { + if (ignoredTransaction.getFilterString().equalsIgnoreCase(transactionName)) { + return true; + } + } + + for (final FilterString ignoredTransaction : ignoredTransactions) { + + try { + if (ignoredTransaction.matches(transactionName)) { + return true; + } + } catch (Throwable t) { + // ignore invalid regex + } + } + + return false; + } } diff --git a/sentry/src/main/java/io/sentry/util/UUIDGenerator.java b/sentry/src/main/java/io/sentry/util/UUIDGenerator.java new file mode 100644 index 00000000000..cbe90f96d83 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/UUIDGenerator.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.sentry.util; + +import java.util.UUID; + +/** + * Utility class for generating UUIDs and half-length (1 long) UUIDs. Adapted from `java.util.UUID` + * to use a faster random number generator. + */ +public final class UUIDGenerator { + + @SuppressWarnings("NarrowingCompoundAssignment") + public static long randomHalfLengthUUID() { + Random random = SentryRandom.current(); + byte[] randomBytes = new byte[8]; + random.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + + long msb = 0; + + for (int i = 0; i < 8; i++) msb = (msb << 8) | (randomBytes[i] & 0xff); + + return msb; + } + + @SuppressWarnings("NarrowingCompoundAssignment") + public static UUID randomUUID() { + Random random = SentryRandom.current(); + byte[] randomBytes = new byte[16]; + random.nextBytes(randomBytes); + randomBytes[6] &= 0x0f; /* clear version */ + randomBytes[6] |= 0x40; /* set to version 4 */ + randomBytes[8] &= 0x3f; /* clear variant */ + randomBytes[8] |= (byte) 0x80; /* set to IETF variant */ + + long msb = 0; + long lsb = 0; + + for (int i = 0; i < 8; i++) msb = (msb << 8) | (randomBytes[i] & 0xff); + + for (int i = 8; i < 16; i++) lsb = (lsb << 8) | (randomBytes[i] & 0xff); + + return new UUID(msb, lsb); + } +} diff --git a/sentry/src/main/java/io/sentry/util/UUIDStringUtils.java b/sentry/src/main/java/io/sentry/util/UUIDStringUtils.java new file mode 100644 index 00000000000..ffff5a8155a --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/UUIDStringUtils.java @@ -0,0 +1,141 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 Jon Chambers + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.sentry.util; + +import java.util.Arrays; +import java.util.UUID; + +/** + * Utility class to convert UUIDs and longs to Sentry ID Strings + * + *

Adapted from Jon Chambers' work here. + */ +public final class UUIDStringUtils { + + private static final int SENTRY_UUID_STRING_LENGTH = 32; + private static final int SENTRY_SPAN_UUID_STRING_LENGTH = 16; + + private static final char[] HEX_DIGITS = + new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static final long[] HEX_VALUES = new long[128]; + + static { + Arrays.fill(HEX_VALUES, -1); + + HEX_VALUES['0'] = 0x0; + HEX_VALUES['1'] = 0x1; + HEX_VALUES['2'] = 0x2; + HEX_VALUES['3'] = 0x3; + HEX_VALUES['4'] = 0x4; + HEX_VALUES['5'] = 0x5; + HEX_VALUES['6'] = 0x6; + HEX_VALUES['7'] = 0x7; + HEX_VALUES['8'] = 0x8; + HEX_VALUES['9'] = 0x9; + + HEX_VALUES['a'] = 0xa; + HEX_VALUES['b'] = 0xb; + HEX_VALUES['c'] = 0xc; + HEX_VALUES['d'] = 0xd; + HEX_VALUES['e'] = 0xe; + HEX_VALUES['f'] = 0xf; + + HEX_VALUES['A'] = 0xa; + HEX_VALUES['B'] = 0xb; + HEX_VALUES['C'] = 0xc; + HEX_VALUES['D'] = 0xd; + HEX_VALUES['E'] = 0xe; + HEX_VALUES['F'] = 0xf; + } + + public static String toSentryIdString(final UUID uuid) { + + final long mostSignificantBits = uuid.getMostSignificantBits(); + final long leastSignificantBits = uuid.getLeastSignificantBits(); + + return toSentryIdString(mostSignificantBits, leastSignificantBits); + } + + public static String toSentryIdString(long mostSignificantBits, long leastSignificantBits) { + final char[] uuidChars = new char[SENTRY_UUID_STRING_LENGTH]; + + fillMostSignificantBits(uuidChars, mostSignificantBits); + + uuidChars[16] = HEX_DIGITS[(int) ((leastSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[17] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[18] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[19] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[20] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[21] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[22] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[23] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[24] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[25] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[26] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[27] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[28] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[29] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[30] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[31] = HEX_DIGITS[(int) (leastSignificantBits & 0x000000000000000fL)]; + + return new String(uuidChars); + } + + public static String toSentrySpanIdString(final UUID uuid) { + + final long mostSignificantBits = uuid.getMostSignificantBits(); + return toSentrySpanIdString(mostSignificantBits); + } + + public static String toSentrySpanIdString(long mostSignificantBits) { + final char[] uuidChars = new char[SENTRY_SPAN_UUID_STRING_LENGTH]; + + fillMostSignificantBits(uuidChars, mostSignificantBits); + + return new String(uuidChars); + } + + private static void fillMostSignificantBits( + final char[] uuidChars, final long mostSignificantBits) { + uuidChars[0] = HEX_DIGITS[(int) ((mostSignificantBits & 0xf000000000000000L) >>> 60)]; + uuidChars[1] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0f00000000000000L) >>> 56)]; + uuidChars[2] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00f0000000000000L) >>> 52)]; + uuidChars[3] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000f000000000000L) >>> 48)]; + uuidChars[4] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000f00000000000L) >>> 44)]; + uuidChars[5] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000f0000000000L) >>> 40)]; + uuidChars[6] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000f000000000L) >>> 36)]; + uuidChars[7] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000f00000000L) >>> 32)]; + uuidChars[8] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000f0000000L) >>> 28)]; + uuidChars[9] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000f000000L) >>> 24)]; + uuidChars[10] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000f00000L) >>> 20)]; + uuidChars[11] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000f0000L) >>> 16)]; + uuidChars[12] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000000f000L) >>> 12)]; + uuidChars[13] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000000f00L) >>> 8)]; + uuidChars[14] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000000f0L) >>> 4)]; + uuidChars[15] = HEX_DIGITS[(int) (mostSignificantBits & 0x000000000000000fL)]; + } +} diff --git a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java similarity index 80% rename from sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java rename to sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java index cf763b49592..81af056e711 100644 --- a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull; @ApiStatus.Internal -public interface IMainThreadChecker { +public interface IThreadChecker { boolean isMainThread(final long threadId); @@ -31,4 +31,11 @@ public interface IMainThreadChecker { * @return true if it is the main thread or false otherwise */ boolean isMainThread(final @NotNull SentryThread sentryThread); + + /** + * Returns the system id of the current thread. Currently only used for Android. + * + * @return the current thread system id. + */ + long currentThreadSystemId(); } diff --git a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java similarity index 67% rename from sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java rename to sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java index 2248e363a4a..b1497d17e7d 100644 --- a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java @@ -5,11 +5,11 @@ import org.jetbrains.annotations.NotNull; @ApiStatus.Internal -public final class NoOpMainThreadChecker implements IMainThreadChecker { +public final class NoOpThreadChecker implements IThreadChecker { - private static final NoOpMainThreadChecker instance = new NoOpMainThreadChecker(); + private static final NoOpThreadChecker instance = new NoOpThreadChecker(); - public static NoOpMainThreadChecker getInstance() { + public static NoOpThreadChecker getInstance() { return instance; } @@ -32,4 +32,9 @@ public boolean isMainThread() { public boolean isMainThread(@NotNull SentryThread sentryThread) { return false; } + + @Override + public long currentThreadSystemId() { + return 0; + } } diff --git a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java similarity index 78% rename from sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java rename to sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java index c81ccbd6683..bfa8aac139e 100644 --- a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java @@ -12,16 +12,16 @@ *

We're gonna educate people through the docs. */ @ApiStatus.Internal -public final class MainThreadChecker implements IMainThreadChecker { +public final class ThreadChecker implements IThreadChecker { private static final long mainThreadId = Thread.currentThread().getId(); - private static final MainThreadChecker instance = new MainThreadChecker(); + private static final ThreadChecker instance = new ThreadChecker(); - public static MainThreadChecker getInstance() { + public static ThreadChecker getInstance() { return instance; } - private MainThreadChecker() {} + private ThreadChecker() {} @Override public boolean isMainThread(long threadId) { @@ -43,4 +43,9 @@ public boolean isMainThread(final @NotNull SentryThread sentryThread) { final Long threadId = sentryThread.getId(); return threadId != null && isMainThread(threadId); } + + @Override + public long currentThreadSystemId() { + return Thread.currentThread().getId(); + } } diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index c24731e92a7..8beae33668b 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -299,7 +299,6 @@ class BaggageTest { baggage.environment = null baggage.transaction = null baggage.userId = null - baggage.userSegment = null assertEquals("", baggage.toHeaderString(null)) } @@ -317,11 +316,10 @@ class BaggageTest { baggage.setEnvironment("production") baggage.setTransaction("TX") baggage.setUserId(userId) - baggage.setUserSegment("segmentA") baggage.setSampleRate((1.0 / 3.0).toString()) baggage.setSampled("true") - assertEquals("sentry-environment=production,sentry-public_key=$publicKey,sentry-release=1.0-rc.1,sentry-sample_rate=0.3333333333333333,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=TX,sentry-user_id=$userId,sentry-user_segment=segmentA", baggage.toHeaderString(null)) + assertEquals("sentry-environment=production,sentry-public_key=$publicKey,sentry-release=1.0-rc.1,sentry-sample_rate=0.3333333333333333,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=TX,sentry-user_id=$userId", baggage.toHeaderString(null)) } @Test diff --git a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt index bac143812a8..658e41149bc 100644 --- a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt +++ b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt @@ -144,6 +144,38 @@ class BreadcrumbTest { assertFalse(breadcrumb.data.containsKey("status_code")) } + @Test + fun `creates HTTP breadcrumb with WARNING level if status code is 4xx`() { + val breadcrumb = Breadcrumb.http("http://example.com", "POST", 417) + assertEquals("http://example.com", breadcrumb.data["url"]) + assertEquals("POST", breadcrumb.data["method"]) + assertEquals("http", breadcrumb.type) + assertEquals("http", breadcrumb.category) + assertEquals(SentryLevel.WARNING, breadcrumb.level) + } + + @Test + fun `creates HTTP breadcrumb with error level if status code is 5xx`() { + val breadcrumb = Breadcrumb.http("http://example.com", "POST", 502) + assertEquals("http://example.com", breadcrumb.data["url"]) + assertEquals("POST", breadcrumb.data["method"]) + assertEquals("http", breadcrumb.type) + assertEquals("http", breadcrumb.category) + assertEquals(502, breadcrumb.data["status_code"]) + assertEquals(SentryLevel.ERROR, breadcrumb.level) + } + + @Test + fun `creates HTTP breadcrumb with null level if status code is not 5xx or 4xx`() { + val breadcrumb = Breadcrumb.http("http://example.com", "POST", 200) + assertEquals("http://example.com", breadcrumb.data["url"]) + assertEquals("POST", breadcrumb.data["method"]) + assertEquals("http", breadcrumb.type) + assertEquals("http", breadcrumb.category) + assertEquals(200, breadcrumb.data["status_code"]) + assertEquals(null, breadcrumb.level) + } + @Test fun `creates navigation breadcrumb`() { val breadcrumb = Breadcrumb.navigation("from", "to") diff --git a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt index 2e8c8d71fc8..db113fa009c 100644 --- a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt @@ -22,11 +22,13 @@ class CheckInSerializationTest { fun getSut(type: MonitorScheduleType): CheckIn { return CheckIn("some_slug", CheckInStatus.ERROR).apply { - contexts.trace = TransactionContext.fromPropagationContext( - PropagationContext().also { - it.traceId = SentryId("f382e3180c714217a81371f8c644aefe") - it.spanId = SpanId("85694b9f567145a6") - } + contexts.setTrace( + TransactionContext.fromPropagationContext( + PropagationContext().also { + it.traceId = SentryId("f382e3180c714217a81371f8c644aefe") + it.spanId = SpanId("85694b9f567145a6") + } + ) ) duration = 12.3 environment = "env" diff --git a/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt b/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt new file mode 100644 index 00000000000..b70e9506a8f --- /dev/null +++ b/sentry/src/test/java/io/sentry/CombinedContextsViewTest.kt @@ -0,0 +1,568 @@ +package io.sentry + +import io.sentry.protocol.App +import io.sentry.protocol.Browser +import io.sentry.protocol.Contexts +import io.sentry.protocol.Device +import io.sentry.protocol.Gpu +import io.sentry.protocol.OperatingSystem +import io.sentry.protocol.Response +import io.sentry.protocol.SentryRuntime +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class CombinedContextsViewTest { + + private class Fixture { + lateinit var current: Contexts + lateinit var isolation: Contexts + lateinit var global: Contexts + + fun getSut(): CombinedContextsView { + current = Contexts() + isolation = Contexts() + global = Contexts() + + return CombinedContextsView(global, isolation, current, ScopeType.ISOLATION) + } + } + + private val fixture = Fixture() + + @Test + fun `uses default context CURRENT`() { + fixture.getSut() + val combined = CombinedContextsView(fixture.global, fixture.isolation, fixture.current, ScopeType.CURRENT) + combined.setTrace(SpanContext("some")) + assertEquals("some", fixture.current.trace?.op) + } + + @Test + fun `uses default context ISOLATION`() { + fixture.getSut() + val combined = CombinedContextsView(fixture.global, fixture.isolation, fixture.current, ScopeType.ISOLATION) + combined.setTrace(SpanContext("some")) + assertEquals("some", fixture.isolation.trace?.op) + } + + @Test + fun `uses default context GLOBAL`() { + fixture.getSut() + val combined = CombinedContextsView(fixture.global, fixture.isolation, fixture.current, ScopeType.GLOBAL) + combined.setTrace(SpanContext("some")) + assertEquals("some", fixture.global.trace?.op) + } + + @Test + fun `prefers trace from current context`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + fixture.isolation.setTrace(SpanContext("isolation")) + fixture.global.setTrace(SpanContext("global")) + + assertEquals("current", combined.trace?.op) + } + + @Test + fun `uses isolation trace if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setTrace(SpanContext("isolation")) + fixture.global.setTrace(SpanContext("global")) + + assertEquals("isolation", combined.trace?.op) + } + + @Test + fun `uses global trace if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setTrace(SpanContext("global")) + + assertEquals("global", combined.trace?.op) + } + + @Test + fun `sets trace on default context`() { + val combined = fixture.getSut() + combined.setTrace(SpanContext("some")) + + assertNull(fixture.current.trace) + assertEquals("some", fixture.isolation.trace?.op) + assertNull(fixture.global.trace) + } + + @Test + fun `prefers app from current context`() { + val combined = fixture.getSut() + fixture.current.setApp(App().also { it.appName = "current" }) + fixture.isolation.setApp(App().also { it.appName = "isolation" }) + fixture.global.setApp(App().also { it.appName = "global" }) + + assertEquals("current", combined.app?.appName) + } + + @Test + fun `uses isolation app if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setApp(App().also { it.appName = "isolation" }) + fixture.global.setApp(App().also { it.appName = "global" }) + + assertEquals("isolation", combined.app?.appName) + } + + @Test + fun `uses global app if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setApp(App().also { it.appName = "global" }) + + assertEquals("global", combined.app?.appName) + } + + @Test + fun `sets app on default context`() { + val combined = fixture.getSut() + combined.setApp(App().also { it.appName = "some" }) + + assertNull(fixture.current.app) + assertEquals("some", fixture.isolation.app?.appName) + assertNull(fixture.global.app) + } + + @Test + fun `prefers browser from current context`() { + val combined = fixture.getSut() + fixture.current.setBrowser(Browser().also { it.name = "current" }) + fixture.isolation.setBrowser(Browser().also { it.name = "isolation" }) + fixture.global.setBrowser(Browser().also { it.name = "global" }) + + assertEquals("current", combined.browser?.name) + } + + @Test + fun `uses isolation browser if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setBrowser(Browser().also { it.name = "isolation" }) + fixture.global.setBrowser(Browser().also { it.name = "global" }) + + assertEquals("isolation", combined.browser?.name) + } + + @Test + fun `uses global browser if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setBrowser(Browser().also { it.name = "global" }) + + assertEquals("global", combined.browser?.name) + } + + @Test + fun `sets browser on default context`() { + val combined = fixture.getSut() + combined.setBrowser(Browser().also { it.name = "some" }) + + assertNull(fixture.current.browser) + assertEquals("some", fixture.isolation.browser?.name) + assertNull(fixture.global.browser) + } + + @Test + fun `prefers device from current context`() { + val combined = fixture.getSut() + fixture.current.setDevice(Device().also { it.name = "current" }) + fixture.isolation.setDevice(Device().also { it.name = "isolation" }) + fixture.global.setDevice(Device().also { it.name = "global" }) + + assertEquals("current", combined.device?.name) + } + + @Test + fun `uses isolation device if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setDevice(Device().also { it.name = "isolation" }) + fixture.global.setDevice(Device().also { it.name = "global" }) + + assertEquals("isolation", combined.device?.name) + } + + @Test + fun `uses global device if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setDevice(Device().also { it.name = "global" }) + + assertEquals("global", combined.device?.name) + } + + @Test + fun `sets device on default context`() { + val combined = fixture.getSut() + combined.setDevice(Device().also { it.name = "some" }) + + assertNull(fixture.current.device) + assertEquals("some", fixture.isolation.device?.name) + assertNull(fixture.global.device) + } + + @Test + fun `prefers operatingSystem from current context`() { + val combined = fixture.getSut() + fixture.current.setOperatingSystem(OperatingSystem().also { it.name = "current" }) + fixture.isolation.setOperatingSystem(OperatingSystem().also { it.name = "isolation" }) + fixture.global.setOperatingSystem(OperatingSystem().also { it.name = "global" }) + + assertEquals("current", combined.operatingSystem?.name) + } + + @Test + fun `uses isolation operatingSystem if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setOperatingSystem(OperatingSystem().also { it.name = "isolation" }) + fixture.global.setOperatingSystem(OperatingSystem().also { it.name = "global" }) + + assertEquals("isolation", combined.operatingSystem?.name) + } + + @Test + fun `uses global operatingSystem if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setOperatingSystem(OperatingSystem().also { it.name = "global" }) + + assertEquals("global", combined.operatingSystem?.name) + } + + @Test + fun `sets operatingSystem on default context`() { + val combined = fixture.getSut() + combined.setOperatingSystem(OperatingSystem().also { it.name = "some" }) + + assertNull(fixture.current.operatingSystem) + assertEquals("some", fixture.isolation.operatingSystem?.name) + assertNull(fixture.global.operatingSystem) + } + + @Test + fun `prefers runtime from current context`() { + val combined = fixture.getSut() + fixture.current.setRuntime(SentryRuntime().also { it.name = "current" }) + fixture.isolation.setRuntime(SentryRuntime().also { it.name = "isolation" }) + fixture.global.setRuntime(SentryRuntime().also { it.name = "global" }) + + assertEquals("current", combined.runtime?.name) + } + + @Test + fun `uses isolation runtime if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setRuntime(SentryRuntime().also { it.name = "isolation" }) + fixture.global.setRuntime(SentryRuntime().also { it.name = "global" }) + + assertEquals("isolation", combined.runtime?.name) + } + + @Test + fun `uses global runtime if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setRuntime(SentryRuntime().also { it.name = "global" }) + + assertEquals("global", combined.runtime?.name) + } + + @Test + fun `sets runtime on default context`() { + val combined = fixture.getSut() + combined.setRuntime(SentryRuntime().also { it.name = "some" }) + + assertNull(fixture.current.runtime) + assertEquals("some", fixture.isolation.runtime?.name) + assertNull(fixture.global.runtime) + } + + @Test + fun `prefers gpu from current context`() { + val combined = fixture.getSut() + fixture.current.setGpu(Gpu().also { it.name = "current" }) + fixture.isolation.setGpu(Gpu().also { it.name = "isolation" }) + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertEquals("current", combined.gpu?.name) + } + + @Test + fun `uses isolation gpu if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setGpu(Gpu().also { it.name = "isolation" }) + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertEquals("isolation", combined.gpu?.name) + } + + @Test + fun `uses global gpu if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertEquals("global", combined.gpu?.name) + } + + @Test + fun `sets gpu on default context`() { + val combined = fixture.getSut() + combined.setGpu(Gpu().also { it.name = "some" }) + + assertNull(fixture.current.gpu) + assertEquals("some", fixture.isolation.gpu?.name) + assertNull(fixture.global.gpu) + } + + @Test + fun `prefers response from current context`() { + val combined = fixture.getSut() + fixture.current.setResponse(Response().also { it.cookies = "current" }) + fixture.isolation.setResponse(Response().also { it.cookies = "isolation" }) + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + assertEquals("current", combined.response?.cookies) + } + + @Test + fun `uses isolation response if current context does not have it`() { + val combined = fixture.getSut() + fixture.isolation.setResponse(Response().also { it.cookies = "isolation" }) + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + assertEquals("isolation", combined.response?.cookies) + } + + @Test + fun `uses global response if current and isolation context do not have it`() { + val combined = fixture.getSut() + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + assertEquals("global", combined.response?.cookies) + } + + @Test + fun `sets response on default context`() { + val combined = fixture.getSut() + combined.setResponse(Response().also { it.cookies = "some" }) + + assertNull(fixture.current.response) + assertEquals("some", fixture.isolation.response?.cookies) + assertNull(fixture.global.response) + } + + @Test + fun `withResponse is executed on current if present`() { + val combined = fixture.getSut() + fixture.current.setResponse(Response().also { it.cookies = "current" }) + fixture.isolation.setResponse(Response().also { it.cookies = "isolation" }) + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + combined.withResponse { response -> + response.cookies = "updated" + } + + assertEquals("updated", fixture.current.response?.cookies) + assertEquals("isolation", fixture.isolation.response?.cookies) + assertEquals("global", fixture.global.response?.cookies) + } + + @Test + fun `withResponse is executed on isolation if current not present`() { + val combined = fixture.getSut() + fixture.isolation.setResponse(Response().also { it.cookies = "isolation" }) + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + combined.withResponse { response -> + response.cookies = "updated" + } + + assertNull(fixture.current.response) + assertEquals("updated", fixture.isolation.response?.cookies) + assertEquals("global", fixture.global.response?.cookies) + } + + @Test + fun `withResponse is executed on global if current and isoaltion not present`() { + val combined = fixture.getSut() + fixture.global.setResponse(Response().also { it.cookies = "global" }) + + combined.withResponse { response -> + response.cookies = "updated" + } + + assertNull(fixture.current.response) + assertNull(fixture.isolation.response) + assertEquals("updated", fixture.global.response?.cookies) + } + + @Test + fun `withResponse is executed on default if not present anywhere`() { + val combined = fixture.getSut() + + combined.withResponse { response -> + response.cookies = "updated" + } + + assertNull(fixture.current.response) + assertEquals("updated", fixture.isolation.response?.cookies) + assertNull(fixture.global.response) + } + + @Test + fun `size combines contexts`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + fixture.isolation.setApp(App().also { it.appName = "isolation" }) + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertEquals(3, combined.size) + } + + @Test + fun `size considers overrides`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + fixture.isolation.setTrace(SpanContext("isolation")) + fixture.global.setTrace(SpanContext("global")) + + assertEquals(1, combined.size) + } + + @Test + fun `isEmpty`() { + val combined = fixture.getSut() + assertTrue(combined.isEmpty) + } + + @Test + fun `isNotEmpty if current has value`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + + assertFalse(combined.isEmpty) + } + + @Test + fun `isNotEmpty if isolation has value`() { + val combined = fixture.getSut() + fixture.isolation.setApp(App().also { it.appName = "isolation" }) + + assertFalse(combined.isEmpty) + } + + @Test + fun `isNotEmpty if global has value`() { + val combined = fixture.getSut() + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertFalse(combined.isEmpty) + } + + @Test + fun `containsKey false`() { + val combined = fixture.getSut() + assertFalse(combined.containsKey("trace")) + } + + @Test + fun `containsKey current`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + assertTrue(combined.containsKey("trace")) + } + + @Test + fun `containsKey isolation`() { + val combined = fixture.getSut() + fixture.isolation.setTrace(SpanContext("isolation")) + assertTrue(combined.containsKey("trace")) + } + + @Test + fun `containsKey global`() { + val combined = fixture.getSut() + fixture.global.setTrace(SpanContext("global")) + assertTrue(combined.containsKey("trace")) + } + + @Test + fun `keys combines contexts`() { + val combined = fixture.getSut() + fixture.current.setTrace(SpanContext("current")) + fixture.isolation.setApp(App().also { it.appName = "isolation" }) + fixture.global.setGpu(Gpu().also { it.name = "global" }) + + assertEquals(listOf("app", "gpu", "trace"), combined.keys().toList().sorted()) + } + + @Test + fun `entrySet combines contexts`() { + val combined = fixture.getSut() + val trace = SpanContext("current") + fixture.current.setTrace(trace) + val app = App().also { it.appName = "isolation" } + fixture.isolation.setApp(app) + val gpu = Gpu().also { it.name = "global" } + fixture.global.setGpu(gpu) + + val entrySet = combined.entrySet() + assertEquals(3, entrySet.size) + assertNotNull(entrySet.firstOrNull { it.key == "trace" && it.value == trace }) + assertNotNull(entrySet.firstOrNull { it.key == "app" && it.value == app }) + assertNotNull(entrySet.firstOrNull { it.key == "gpu" && it.value == gpu }) + } + + @Test + fun `get prefers current`() { + val combined = fixture.getSut() + fixture.current.put("test", "current") + fixture.isolation.put("test", "isolation") + fixture.global.put("test", "global") + + assertEquals("current", combined.get("test")) + } + + @Test + fun `get uses isolation if not in current`() { + val combined = fixture.getSut() + fixture.isolation.put("test", "isolation") + fixture.global.put("test", "global") + + assertEquals("isolation", combined.get("test")) + } + + @Test + fun `get uses global if not in current or isolation`() { + val combined = fixture.getSut() + fixture.global.put("test", "global") + + assertEquals("global", combined.get("test")) + } + + @Test + fun `put stores in default context`() { + val combined = fixture.getSut() + combined.put("test", "aValue") + + assertNull(fixture.current.get("test")) + assertEquals("aValue", fixture.isolation.get("test")) + assertNull(fixture.global.get("test")) + } + + @Test + fun `remove removes from default context`() { + val combined = fixture.getSut() + fixture.current.put("test", "current") + fixture.isolation.put("test", "isolation") + fixture.global.put("test", "global") + + combined.remove("test") + + assertEquals("current", fixture.current.get("test")) + assertNull(fixture.isolation.get("test")) + assertEquals("global", fixture.global.get("test")) + } +} diff --git a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt new file mode 100644 index 00000000000..cbb8ed0e9cd --- /dev/null +++ b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt @@ -0,0 +1,1158 @@ +package io.sentry + +import io.sentry.protocol.Device +import io.sentry.protocol.Request +import io.sentry.protocol.SentryId +import io.sentry.protocol.User +import io.sentry.test.createTestScopes +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Assert.assertNotEquals +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.lang.RuntimeException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertSame + +class CombinedScopeViewTest { + + private class Fixture { + lateinit var globalScope: IScope + lateinit var isolationScope: IScope + lateinit var scope: IScope + lateinit var options: SentryOptions + lateinit var scopes: IScopes + + fun getSut(options: SentryOptions = SentryOptions()): CombinedScopeView { + options.dsn = "https://key@sentry.io/proj" + options.release = "0.1" + this.options = options + globalScope = Scope(options) + isolationScope = Scope(options) + scope = Scope(options) + scopes = createTestScopes(options, scope = scope, isolationScope = isolationScope, globalScope = globalScope) + + return CombinedScopeView(globalScope, isolationScope, scope) + } + } + + private val fixture = Fixture() + + @Test + fun `adds breadcrumbs from all scopes in sorted order`() { + val combined = fixture.getSut() + + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global 1")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation 1")) + fixture.scope.addBreadcrumb(Breadcrumb.info("current 1")) + + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global 2")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation 2")) + fixture.scope.addBreadcrumb(Breadcrumb.info("current 2")) + + val breadcrumbs = combined.breadcrumbs + assertEquals("global 1", breadcrumbs.poll().message) + assertEquals("isolation 1", breadcrumbs.poll().message) + assertEquals("current 1", breadcrumbs.poll().message) + assertEquals("global 2", breadcrumbs.poll().message) + assertEquals("isolation 2", breadcrumbs.poll().message) + assertEquals("current 2", breadcrumbs.poll().message) + } + + @Test + fun `oldest breadcrumbs are dropped first`() { + val options = SentryOptions().also { it.maxBreadcrumbs = 5 } + val combined = fixture.getSut(options) + + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global 1")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation 1")) + fixture.scope.addBreadcrumb(Breadcrumb.info("current 1")) + + fixture.globalScope.addBreadcrumb(Breadcrumb.info("global 2")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolation 2")) + fixture.scope.addBreadcrumb(Breadcrumb.info("current 2")) + + val breadcrumbs = combined.breadcrumbs +// assertEquals("global 1", breadcrumbs.poll().message) <-- was dropped + assertEquals("isolation 1", breadcrumbs.poll().message) + assertEquals("current 1", breadcrumbs.poll().message) + assertEquals("global 2", breadcrumbs.poll().message) + assertEquals("isolation 2", breadcrumbs.poll().message) + assertEquals("current 2", breadcrumbs.poll().message) + + fixture.scope.addBreadcrumb(Breadcrumb.info("current 3")) + fixture.scope.addBreadcrumb(Breadcrumb.info("current 4")) + + val breadcrumbs2 = combined.breadcrumbs +// assertEquals("global 1", breadcrumbs.poll().message) <-- was dropped +// assertEquals("isolation 1", breadcrumbs2.poll().message) <-- dropped +// assertEquals("current 1", breadcrumbs2.poll().message) <-- dropped + assertEquals("global 2", breadcrumbs2.poll().message) + assertEquals("isolation 2", breadcrumbs2.poll().message) + assertEquals("current 2", breadcrumbs2.poll().message) + assertEquals("current 3", breadcrumbs2.poll().message) + assertEquals("current 4", breadcrumbs2.poll().message) + } + + @Test + fun `can add breadcrumb with hint`() { + var capturedHint: Hint? = null + val combined = fixture.getSut( + SentryOptions().also { + it.beforeBreadcrumb = + SentryOptions.BeforeBreadcrumbCallback { breadcrumb: Breadcrumb, hint: Hint -> + capturedHint = hint + breadcrumb + } + } + ) + + combined.addBreadcrumb(Breadcrumb.info("aBreadcrumb"), Hint().also { it.set("aTest", "aValue") }) + + assertNotNull(capturedHint) + assertEquals("aValue", capturedHint?.get("aTest")) + + val breadcrumbs = combined.breadcrumbs + assertEquals(1, breadcrumbs.size) + assertEquals("aBreadcrumb", breadcrumbs.first().message) + } + + @Test + fun `adds breadcrumb to default scope`() { + val combined = fixture.getSut() + combined.addBreadcrumb(Breadcrumb.info("aBreadcrumb")) + + assertEquals(ScopeType.ISOLATION, combined.options.defaultScopeType) + assertEquals(0, fixture.scope.breadcrumbs.size) + assertEquals(1, fixture.isolationScope.breadcrumbs.size) + assertEquals(0, fixture.globalScope.breadcrumbs.size) + } + + @Test + fun `clears breadcrumbs only from default scope`() { + val combined = fixture.getSut() + fixture.scope.addBreadcrumb(Breadcrumb.info("scopeBreadcrumb")) + fixture.isolationScope.addBreadcrumb(Breadcrumb.info("isolationBreadcrumb")) + fixture.globalScope.addBreadcrumb(Breadcrumb.info("globalBreadcrumb")) + + combined.clearBreadcrumbs() + + assertEquals(ScopeType.ISOLATION, combined.options.defaultScopeType) + assertEquals(1, fixture.scope.breadcrumbs.size) + assertEquals(0, fixture.isolationScope.breadcrumbs.size) + assertEquals(1, fixture.globalScope.breadcrumbs.size) + } + + @Test + fun `event processors from options are not returned`() { + val options = SentryOptions().also { + it.addEventProcessor(MainEventProcessor(it)) + } + val combined = fixture.getSut(options) + + assertEquals(0, combined.eventProcessors.size) + } + + @Test + fun `event processors from all scopes are returned in order`() { + val combined = fixture.getSut() + + val first = TestEventProcessor(0).also { fixture.scope.addEventProcessor(it) } + val second = TestEventProcessor(1000).also { fixture.globalScope.addEventProcessor(it) } + val third = TestEventProcessor(2000).also { fixture.isolationScope.addEventProcessor(it) } + val fourth = TestEventProcessor(3000).also { fixture.scope.addEventProcessor(it) } + + val eventProcessors = combined.eventProcessors + + assertEquals(first, eventProcessors[0]) + assertEquals(second, eventProcessors[1]) + assertEquals(third, eventProcessors[2]) + assertEquals(fourth, eventProcessors[3]) + } + + @Test + fun `adds event processor to default scope`() { + val combined = fixture.getSut() + + val eventProcessor = MainEventProcessor(fixture.options) + combined.addEventProcessor(eventProcessor) + + assertEquals(ScopeType.ISOLATION, combined.options.defaultScopeType) + assertFalse(fixture.scope.eventProcessors.contains(eventProcessor)) + assertTrue(fixture.isolationScope.eventProcessors.contains(eventProcessor)) + assertFalse(fixture.globalScope.eventProcessors.contains(eventProcessor)) + } + + @Test + fun `prefers level from current scope`() { + val combined = fixture.getSut() + fixture.scope.level = SentryLevel.DEBUG + fixture.isolationScope.level = SentryLevel.INFO + fixture.globalScope.level = SentryLevel.WARNING + + assertEquals(SentryLevel.DEBUG, combined.level) + } + + @Test + fun `uses isolation scope level if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.level = SentryLevel.INFO + fixture.globalScope.level = SentryLevel.WARNING + + assertEquals(SentryLevel.INFO, combined.level) + } + + @Test + fun `uses global scope level if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.level = SentryLevel.WARNING + + assertEquals(SentryLevel.WARNING, combined.level) + } + + @Test + fun `returns null level if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.level) + } + + @Test + fun `setLevel modifies default scope`() { + val combined = fixture.getSut() + combined.level = SentryLevel.ERROR + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.level) + assertEquals(SentryLevel.ERROR, fixture.isolationScope.level) + assertNull(fixture.globalScope.level) + } + + @Test + fun `prefers transaction name from current scope`() { + val combined = fixture.getSut() + fixture.scope.setTransaction("scopeTransaction") + fixture.isolationScope.setTransaction("isolationTransaction") + fixture.globalScope.setTransaction("globalTransaction") + + assertEquals("scopeTransaction", combined.transactionName) + } + + @Test + fun `uses isolation transaction name if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.setTransaction("isolationTransaction") + fixture.globalScope.setTransaction("globalTransaction") + + assertEquals("isolationTransaction", combined.transactionName) + } + + @Test + fun `uses global transaction name if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.setTransaction("globalTransaction") + + assertEquals("globalTransaction", combined.transactionName) + } + + @Test + fun `returns null transaction name if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.transactionName) + } + + @Test + fun `setTransaction(String) modifies default scope`() { + val combined = fixture.getSut() + combined.setTransaction("aTransaction") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.transactionName) + assertEquals("aTransaction", fixture.isolationScope.transactionName) + assertNull(fixture.globalScope.transactionName) + } + + @Test + fun `prefers transaction and span from current scope`() { + val combined = fixture.getSut() + fixture.scope.setTransaction(createTransaction("scopeTransaction")) + fixture.isolationScope.setTransaction(createTransaction("isolationTransaction")) + fixture.globalScope.setTransaction(createTransaction("globalTransaction")) + + assertEquals("scopeTransaction", combined.transaction!!.name) + assertEquals("scopeTransactionSpan", combined.span!!.operation) + } + + @Test + fun `uses isolation scope transaction and span if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.setTransaction(createTransaction("isolationTransaction")) + fixture.globalScope.setTransaction(createTransaction("globalTransaction")) + + assertEquals("isolationTransaction", combined.transaction!!.name) + assertEquals("isolationTransactionSpan", combined.span!!.operation) + } + + @Test + fun `uses global transaction and scope span if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.setTransaction(createTransaction("globalTransaction")) + + assertEquals("globalTransaction", combined.transaction!!.name) + assertEquals("globalTransactionSpan", combined.span!!.operation) + } + + @Test + fun `returns null transaction and span if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.transaction) + assertNull(combined.span) + } + + @Test + fun `setTransaction(ITransaction) modifies default scope`() { + val combined = fixture.getSut() + val tx = createTransaction("aTransaction") + combined.setTransaction(tx) + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.transaction) + assertSame(tx, fixture.isolationScope.transaction) + assertNull(fixture.globalScope.transaction) + } + + @Test + fun `clears transaction from default scope`() { + val combined = fixture.getSut() + fixture.scope.setTransaction(createTransaction("scopeTransaction")) + fixture.isolationScope.setTransaction(createTransaction("isolationTransaction")) + fixture.globalScope.setTransaction(createTransaction("globalTransaction")) + + combined.clearTransaction() + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNotNull(fixture.scope.transaction) + assertNull(fixture.isolationScope.transaction) + assertNotNull(fixture.globalScope.transaction) + } + + @Test + fun `prefers user from current scope`() { + val combined = fixture.getSut() + fixture.scope.user = User().also { it.name = "scopeUser" } + fixture.isolationScope.user = User().also { it.name = "isolationUser" } + fixture.globalScope.user = User().also { it.name = "globalUser" } + + assertEquals("scopeUser", combined.user!!.name) + } + + @Test + fun `uses isolation scope user if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.user = User().also { it.name = "isolationUser" } + fixture.globalScope.user = User().also { it.name = "globalUser" } + + assertEquals("isolationUser", combined.user!!.name) + } + + @Test + fun `uses global scope user if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.user = User().also { it.name = "globalUser" } + + assertEquals("globalUser", combined.user!!.name) + } + + @Test + fun `returns null user if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.user) + } + + @Test + fun `set user modifies default scope`() { + val combined = fixture.getSut() + val user = User().also { it.name = "aUser" } + combined.user = user + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.user) + assertSame(user, fixture.isolationScope.user) + assertNull(fixture.globalScope.user) + } + + @Test + fun `prefers screen from current scope`() { + val combined = fixture.getSut() + fixture.scope.screen = "scopeScreen" + fixture.isolationScope.screen = "isolationScreen" + fixture.globalScope.screen = "globalScreen" + + assertEquals("scopeScreen", combined.screen) + } + + @Test + fun `uses isolation scope screen if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.screen = "isolationScreen" + fixture.globalScope.screen = "globalScreen" + + assertEquals("isolationScreen", combined.screen) + } + + @Test + fun `uses global scope screen if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.screen = "globalScreen" + + assertEquals("globalScreen", combined.screen) + } + + @Test + fun `returns null screen if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.screen) + } + + @Test + fun `set screen modifies default scope`() { + val combined = fixture.getSut() + combined.screen = "aScreen" + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.screen) + assertEquals("aScreen", fixture.isolationScope.screen) + assertNull(fixture.globalScope.screen) + } + + @Test + fun `prefers request from current scope`() { + val combined = fixture.getSut() + fixture.scope.request = Request().also { it.queryString = "scopeRequest" } + fixture.isolationScope.request = Request().also { it.queryString = "isolationRequest" } + fixture.globalScope.request = Request().also { it.queryString = "globalRequest" } + + assertEquals("scopeRequest", combined.request!!.queryString) + } + + @Test + fun `uses isolation scope request if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.request = Request().also { it.queryString = "isolationRequest" } + fixture.globalScope.request = Request().also { it.queryString = "globalRequest" } + + assertEquals("isolationRequest", combined.request!!.queryString) + } + + @Test + fun `uses global scope request if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.request = Request().also { it.queryString = "globalRequest" } + + assertEquals("globalRequest", combined.request!!.queryString) + } + + @Test + fun `returns null request if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.request) + } + + @Test + fun `set request modifies default scope`() { + val combined = fixture.getSut() + val request = Request().also { it.queryString = "aRequest" } + combined.request = request + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.request) + assertSame(request, fixture.isolationScope.request) + assertNull(fixture.globalScope.request) + } + + @Test + fun `clear removes from default scope`() { + val combined = fixture.getSut() + + fixture.scope.level = SentryLevel.DEBUG + fixture.isolationScope.level = SentryLevel.INFO + fixture.globalScope.level = SentryLevel.WARNING + + combined.clear() + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNotNull(fixture.scope.level) + assertNull(fixture.isolationScope.level) + assertNotNull(fixture.globalScope.level) + } + + @Test + fun `tags are combined from all scopes`() { + val combined = fixture.getSut() + + fixture.scope.setTag("scopeTag", "scopeValue") + fixture.isolationScope.setTag("isolationTag", "isolationValue") + fixture.globalScope.setTag("globalTag", "globalValue") + + val tags = combined.tags + assertEquals("scopeValue", tags["scopeTag"]) + assertEquals("isolationValue", tags["isolationTag"]) + assertEquals("globalValue", tags["globalTag"]) + } + + @Test + fun `setTag writes to default scope`() { + val combined = fixture.getSut() + combined.setTag("aTag", "aValue") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.tags["aTag"]) + assertEquals("aValue", fixture.isolationScope.tags["aTag"]) + assertNull(fixture.globalScope.tags["aTag"]) + } + + @Test + fun `prefer current scope value for tags with same key`() { + val combined = fixture.getSut() + + fixture.scope.setTag("aTag", "scopeValue") + fixture.isolationScope.setTag("aTag", "isolationValue") + fixture.globalScope.setTag("aTag", "globalValue") + + assertEquals("scopeValue", combined.tags["aTag"]) + } + + @Test + fun `uses isolation scope value for tags with same key if scope does not have it`() { + val combined = fixture.getSut() + + fixture.isolationScope.setTag("aTag", "isolationValue") + fixture.globalScope.setTag("aTag", "globalValue") + + assertEquals("isolationValue", combined.tags["aTag"]) + } + + @Test + fun `uses global scope value for tags with same key if scope and isolation scope do not have it`() { + val combined = fixture.getSut() + + fixture.globalScope.setTag("aTag", "globalValue") + + assertEquals("globalValue", combined.tags["aTag"]) + } + + @Test + fun `removeTag removes from default scope`() { + val combined = fixture.getSut() + + fixture.scope.setTag("aTag", "scopeValue") + fixture.isolationScope.setTag("aTag", "isolationValue") + fixture.globalScope.setTag("aTag", "globalValue") + + combined.removeTag("aTag") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals("scopeValue", fixture.scope.tags["aTag"]) + assertNull(fixture.isolationScope.tags["aTag"]) + assertEquals("globalValue", fixture.globalScope.tags["aTag"]) + } + + @Test + fun `extras are combined from all scopes`() { + val combined = fixture.getSut() + + fixture.scope.setExtra("scopeExtra", "scopeValue") + fixture.isolationScope.setExtra("isolationExtra", "isolationValue") + fixture.globalScope.setExtra("globalExtra", "globalValue") + + val extras = combined.extras + assertEquals("scopeValue", extras["scopeExtra"]) + assertEquals("isolationValue", extras["isolationExtra"]) + assertEquals("globalValue", extras["globalExtra"]) + } + + @Test + fun `setExtra writes to default scope`() { + val combined = fixture.getSut() + combined.setExtra("someExtra", "aValue") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.extras["someExtra"]) + assertEquals("aValue", fixture.isolationScope.extras["someExtra"]) + assertNull(fixture.globalScope.extras["someExtra"]) + } + + @Test + fun `prefer current scope value for extras with same key`() { + val combined = fixture.getSut() + + fixture.scope.setExtra("someExtra", "scopeValue") + fixture.isolationScope.setExtra("someExtra", "isolationValue") + fixture.globalScope.setExtra("someExtra", "globalValue") + + assertEquals("scopeValue", combined.extras["someExtra"]) + } + + @Test + fun `uses isolation scope value for extras with same key if scope does not have it`() { + val combined = fixture.getSut() + + fixture.isolationScope.setExtra("someExtra", "isolationValue") + fixture.globalScope.setExtra("someExtra", "globalValue") + + assertEquals("isolationValue", combined.extras["someExtra"]) + } + + @Test + fun `uses global scope value for extras with same key if scope and isolation scope do not have it`() { + val combined = fixture.getSut() + + fixture.globalScope.setExtra("someExtra", "globalValue") + + assertEquals("globalValue", combined.extras["someExtra"]) + } + + @Test + fun `removeExtra removes from default scope`() { + val combined = fixture.getSut() + + fixture.scope.setExtra("someExtra", "scopeValue") + fixture.isolationScope.setExtra("someExtra", "isolationValue") + fixture.globalScope.setExtra("someExtra", "globalValue") + + combined.removeExtra("someExtra") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals("scopeValue", fixture.scope.extras["someExtra"]) + assertNull(fixture.isolationScope.extras["someExtra"]) + assertEquals("globalValue", fixture.globalScope.extras["someExtra"]) + } + + @Test + fun `combines context from all scopes`() { + val combined = fixture.getSut() + fixture.scope.setContexts("scopeContext", "scopeValue") + fixture.isolationScope.setContexts("isolationContext", "isolationValue") + fixture.globalScope.setContexts("globalContext", "globalValue") + + val contexts = combined.contexts + assertEquals(mapOf("value" to "scopeValue"), contexts["scopeContext"]) + } + + @Test + fun `current scope context overrides context of other scopes`() { + val combined = fixture.getSut() + fixture.scope.setContexts("someContext", "scopeValue") + fixture.isolationScope.setContexts("someContext", "isolationValue") + fixture.globalScope.setContexts("someContext", "globalValue") + + val contexts = combined.contexts + assertEquals(mapOf("value" to "scopeValue"), contexts["someContext"]) + } + + @Test + fun `isolation scope context overrides global context`() { + val combined = fixture.getSut() + fixture.isolationScope.setContexts("someContext", "isolationValue") + fixture.globalScope.setContexts("someContext", "globalValue") + + val contexts = combined.contexts + assertEquals(mapOf("value" to "isolationValue"), contexts["someContext"]) + } + + @Test + fun `setContexts writes to default scope`() { + val combined = fixture.getSut() + combined.setContexts("aString", "stringValue") + combined.setContexts("aChar", 'c') + combined.setContexts("aNumber", 1) + combined.setContexts("someObject", Device().also { it.brand = "someDeviceBrand" }) + combined.setContexts("someArray", arrayOf("a", "b")) + combined.setContexts("someList", listOf("c", "d", "e")) + + assertNull(fixture.scope.contexts["aString"]) + assertNull(fixture.scope.contexts["aChar"]) + assertNull(fixture.scope.contexts["aNumber"]) + assertNull(fixture.scope.contexts["someObject"]) + assertNull(fixture.scope.contexts["someArray"]) + assertNull(fixture.scope.contexts["someList"]) + + assertEquals(mapOf("value" to "stringValue"), fixture.isolationScope.contexts["aString"]) + assertEquals(mapOf("value" to 'c'), fixture.isolationScope.contexts["aChar"]) + assertEquals(mapOf("value" to 1), fixture.isolationScope.contexts["aNumber"]) + assertEquals("someDeviceBrand", (fixture.isolationScope.contexts["someObject"] as? Device)?.brand) + val arrayValue = (fixture.isolationScope.contexts["someArray"] as? Map)?.get("value") as? Array + assertEquals(2, arrayValue?.size) + assertEquals("a", arrayValue?.get(0)) + assertEquals("b", arrayValue?.get(1)) + val listValue = (fixture.isolationScope.contexts["someList"] as? Map)?.get("value") as? List + assertEquals(3, listValue?.size) + assertEquals("c", listValue?.get(0)) + assertEquals("d", listValue?.get(1)) + assertEquals("e", listValue?.get(2)) + + assertNull(fixture.globalScope.contexts["aString"]) + assertNull(fixture.globalScope.contexts["aChar"]) + assertNull(fixture.globalScope.contexts["aNumber"]) + assertNull(fixture.globalScope.contexts["someObject"]) + assertNull(fixture.globalScope.contexts["someArray"]) + assertNull(fixture.globalScope.contexts["someList"]) + } + + @Test + fun `combines attachments from all scopes`() { + val combined = fixture.getSut() + + fixture.scope.addAttachment(createAttachment("scopeAttachment.png")) + fixture.isolationScope.addAttachment(createAttachment("isolationAttachment.png")) + fixture.globalScope.addAttachment(createAttachment("globalAttachment.png")) + + val attachments = combined.attachments + assertNotNull(attachments.firstOrNull { it.filename == "scopeAttachment.png" }) + assertNotNull(attachments.firstOrNull { it.filename == "isolationAttachment.png" }) + assertNotNull(attachments.firstOrNull { it.filename == "globalAttachment.png" }) + } + + @Test + fun `adds attachment to default scope`() { + val combined = fixture.getSut() + combined.addAttachment(createAttachment("someAttachment.png")) + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.attachments.firstOrNull { it.filename == "someAttachment.png" }) + assertNotNull(fixture.isolationScope.attachments.firstOrNull { it.filename == "someAttachment.png" }) + assertNull(fixture.globalScope.attachments.firstOrNull { it.filename == "someAttachment.png" }) + } + + @Test + fun `clears attachments only from default scope`() { + val combined = fixture.getSut() + + fixture.scope.addAttachment(createAttachment("scopeAttachment.png")) + fixture.isolationScope.addAttachment(createAttachment("isolationAttachment.png")) + fixture.globalScope.addAttachment(createAttachment("globalAttachment.png")) + + combined.clearAttachments() + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNotNull(fixture.scope.attachments.firstOrNull { it.filename == "scopeAttachment.png" }) + assertNull(fixture.isolationScope.attachments.firstOrNull { it.filename == "isolationAttachment.png" }) + assertNotNull(fixture.globalScope.attachments.firstOrNull { it.filename == "globalAttachment.png" }) + } + + @Test + fun `returns options from global scope`() { + val scopeOptions = SentryOptions().also { it.dist = "scopeDist" } + val isolationOptions = SentryOptions().also { it.dist = "isolationDist" } + val globalOptions = SentryOptions().also { it.dist = "globalDist" } + + val combined = CombinedScopeView(Scope(globalOptions), Scope(isolationOptions), Scope(scopeOptions)) + assertEquals("globalDist", combined.options.dist) + } + + @Test + fun `replaces options on global scope`() { + val scopeOptions = SentryOptions().also { it.dist = "scopeDist" } + val isolationOptions = SentryOptions().also { it.dist = "isolationDist" } + val globalOptions = SentryOptions().also { it.dist = "globalDist" } + + val globalScope = Scope(globalOptions) + val isolationScope = Scope(isolationOptions) + val scope = Scope(scopeOptions) + val combined = CombinedScopeView(globalScope, isolationScope, scope) + + val newOptions = SentryOptions().also { it.dist = "newDist" } + combined.replaceOptions(newOptions) + + assertEquals("scopeDist", scope.options.dist) + assertEquals("isolationDist", isolationScope.options.dist) + assertEquals("newDist", globalScope.options.dist) + } + + @Test + fun `prefers client from scope`() { + val combined = fixture.getSut() + + val scopeClient = SentryClient(fixture.options) + fixture.scope.bindClient(scopeClient) + + val isolationClient = SentryClient(fixture.options) + fixture.isolationScope.bindClient(isolationClient) + + val globalClient = SentryClient(fixture.options) + fixture.globalScope.bindClient(globalClient) + + assertSame(scopeClient, combined.client) + } + + @Test + fun `uses isolation scope client if noop on current scope`() { + val combined = fixture.getSut() + fixture.scope.bindClient(NoOpSentryClient.getInstance()) + fixture.isolationScope.bindClient(NoOpSentryClient.getInstance()) + fixture.globalScope.bindClient(NoOpSentryClient.getInstance()) + + val isolationClient = SentryClient(fixture.options) + fixture.isolationScope.bindClient(isolationClient) + + val globalClient = SentryClient(fixture.options) + fixture.globalScope.bindClient(globalClient) + + assertSame(isolationClient, combined.client) + } + + @Test + fun `uses global scope client if noop on current and isolation scope`() { + val combined = fixture.getSut() + fixture.scope.bindClient(NoOpSentryClient.getInstance()) + fixture.isolationScope.bindClient(NoOpSentryClient.getInstance()) + fixture.globalScope.bindClient(NoOpSentryClient.getInstance()) + + val globalClient = SentryClient(fixture.options) + fixture.globalScope.bindClient(globalClient) + + assertSame(globalClient, combined.client) + } + + @Test + fun `binds client to default scope`() { + val combined = fixture.getSut() + fixture.scope.bindClient(NoOpSentryClient.getInstance()) + fixture.isolationScope.bindClient(NoOpSentryClient.getInstance()) + fixture.globalScope.bindClient(NoOpSentryClient.getInstance()) + + val client = SentryClient(fixture.options) + combined.bindClient(client) + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertTrue(fixture.scope.client is NoOpSentryClient) + assertSame(client, fixture.isolationScope.client) + assertTrue(fixture.globalScope.client is NoOpSentryClient) + } + + @Test + fun `getSpecificScope(null) returns scope defined in options CURRENT`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.CURRENT }) + assertSame(fixture.scope, combined.getSpecificScope(null)) + } + + @Test + fun `getSpecificScope(null) returns scope defined in options ISOLATION`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.ISOLATION }) + assertSame(fixture.isolationScope, combined.getSpecificScope(null)) + } + + @Test + fun `getSpecificScope(null) returns scope defined in options GLOBAL`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.GLOBAL }) + assertSame(fixture.globalScope, combined.getSpecificScope(null)) + } + + @Test + fun `getSpecificScope(CURRENT) returns current scope`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.ISOLATION }) + assertSame(fixture.scope, combined.getSpecificScope(ScopeType.CURRENT)) + } + + @Test + fun `getSpecificScope(ISOLATION) returns isolation scope`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.CURRENT }) + assertSame(fixture.isolationScope, combined.getSpecificScope(ScopeType.ISOLATION)) + } + + @Test + fun `getSpecificScope(GLOBAL) returns global scope`() { + val combined = fixture.getSut(SentryOptions().also { it.defaultScopeType = ScopeType.CURRENT }) + assertSame(fixture.globalScope, combined.getSpecificScope(ScopeType.GLOBAL)) + } + + @Test + fun `forwards setSpanContext to global scope`() { + val scope = mock() + val isolationScope = mock() + val globalScope = mock() + val combined = CombinedScopeView(globalScope, isolationScope, scope) + + val options = SentryOptions().also { it.dsn = "https://key@sentry.io/proj" } + whenever(globalScope.options).thenReturn(options) + + val exception = RuntimeException("someEx") + val transaction = createTransaction("aTransaction", createTestScopes(options = options, scope = scope, isolationScope = isolationScope, globalScope = globalScope)) + combined.setSpanContext(exception, transaction, "aTransaction") + + verify(scope, never()).setSpanContext(any(), any(), any()) + verify(isolationScope, never()).setSpanContext(any(), any(), any()) + verify(globalScope).setSpanContext(same(exception), same(transaction), eq("aTransaction")) + } + + @Test + fun `withTransaction uses default scope`() { + val combined = fixture.getSut() + fixture.scope.setTransaction(createTransaction("scopeTransaction")) + fixture.isolationScope.setTransaction(createTransaction("isolationTransaction")) + fixture.globalScope.setTransaction(createTransaction("globalTransaction")) + + var capturedTransaction: ITransaction? = null + combined.withTransaction { transaction -> + capturedTransaction = transaction + } + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals("isolationTransaction", capturedTransaction?.name) + } + + @Test + fun `forwards assignTraceContext to global scope`() { + val scope = mock() + val isolationScope = mock() + val globalScope = mock() + val combined = CombinedScopeView(globalScope, isolationScope, scope) + + val event = SentryEvent() + combined.assignTraceContext(event) + + verify(scope, never()).assignTraceContext(any()) + verify(isolationScope, never()).assignTraceContext(any()) + verify(globalScope).assignTraceContext(same(event)) + } + + @Test + fun `retrieves last event id from global scope`() { + val combined = fixture.getSut() + fixture.scope.lastEventId = SentryId("c81d4e2e-bcf2-11e6-869b-7df92533d2dc") + fixture.isolationScope.lastEventId = SentryId("d81d4e2e-bcf2-11e6-869b-7df92533d2dd") + fixture.globalScope.lastEventId = SentryId("e81d4e2e-bcf2-11e6-869b-7df92533d2de") + + assertEquals("e81d4e2ebcf211e6869b7df92533d2de", combined.lastEventId.toString()) + } + + @Test + fun `sets last event id on all scopes`() { + val combined = fixture.getSut() + combined.lastEventId = SentryId("c81d4e2e-bcf2-11e6-869b-7df92533d2db") + + assertEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.scope.lastEventId.toString()) + assertEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.isolationScope.lastEventId.toString()) + assertEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.globalScope.lastEventId.toString()) + } + + @Test + fun `retrieves propagation context from default scope`() { + val combined = fixture.getSut() + fixture.scope.propagationContext = PropagationContext().also { it.traceId = SentryId("c81d4e2e-bcf2-11e6-869b-7df92533d2dc") } + fixture.isolationScope.propagationContext = PropagationContext().also { it.traceId = SentryId("d81d4e2e-bcf2-11e6-869b-7df92533d2dd") } + fixture.globalScope.propagationContext = PropagationContext().also { it.traceId = SentryId("e81d4e2e-bcf2-11e6-869b-7df92533d2de") } + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals("d81d4e2ebcf211e6869b7df92533d2dd", combined.propagationContext.traceId.toString()) + } + + @Test + fun `sets propagation context on default scope`() { + val combined = fixture.getSut() + + combined.propagationContext = PropagationContext().also { it.traceId = SentryId("c81d4e2e-bcf2-11e6-869b-7df92533d2db") } + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNotEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.scope.propagationContext.traceId.toString()) + assertEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.isolationScope.propagationContext.traceId.toString()) + assertNotEquals("c81d4e2ebcf211e6869b7df92533d2db", fixture.globalScope.propagationContext.traceId.toString()) + } + + @Test + fun `withPropagationContext uses default scope`() { + val combined = fixture.getSut() + fixture.scope.propagationContext = PropagationContext().also { it.traceId = SentryId("c81d4e2e-bcf2-11e6-869b-7df92533d2dc") } + fixture.isolationScope.propagationContext = PropagationContext().also { it.traceId = SentryId("d81d4e2e-bcf2-11e6-869b-7df92533d2dd") } + fixture.globalScope.propagationContext = PropagationContext().also { it.traceId = SentryId("e81d4e2e-bcf2-11e6-869b-7df92533d2de") } + + var capturedPropagationContext: PropagationContext? = null + combined.withPropagationContext { propagationContext -> + capturedPropagationContext = propagationContext + } + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals("d81d4e2ebcf211e6869b7df92533d2dd", capturedPropagationContext?.traceId.toString()) + } + + @Test + fun `starts session on default scope`() { + val combined = fixture.getSut() + + combined.startSession() + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.session) + assertNotNull(fixture.isolationScope.session) + assertNull(fixture.globalScope.session) + } + + @Test + fun `ends session on default scope`() { + val combined = fixture.getSut() + fixture.scope.startSession() + fixture.isolationScope.startSession() + fixture.globalScope.startSession() + + combined.endSession() + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNotNull(fixture.scope.session) + assertNull(fixture.isolationScope.session) + assertNotNull(fixture.globalScope.session) + } + + @Test + fun `prefers session from current scope`() { + val combined = fixture.getSut() + fixture.scope.startSession() + fixture.isolationScope.startSession() + fixture.globalScope.startSession() + + assertSame(fixture.scope.session, combined.session) + } + + @Test + fun `uses isolation scope session if none on current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.startSession() + fixture.globalScope.startSession() + + assertSame(fixture.isolationScope.session, combined.session) + } + + @Test + fun `uses global scope session if none on current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.startSession() + + assertSame(fixture.globalScope.session, combined.session) + } + + @Test + fun `withSession uses default scope`() { + val combined = fixture.getSut() + fixture.scope.startSession() + fixture.isolationScope.startSession() + fixture.globalScope.startSession() + + var capturedSession: Session? = null + combined.withSession { session -> + capturedSession = session + } + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertSame(fixture.isolationScope.session, capturedSession) + } + + @Test + fun `sets fingerprint on default scope`() { + val combined = fixture.getSut() + combined.fingerprint = listOf("aFingerprint") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals(0, fixture.scope.fingerprint.size) + assertEquals(1, fixture.isolationScope.fingerprint.size) + assertEquals(0, fixture.globalScope.fingerprint.size) + } + + @Test + fun `prefers fingerprint from current scope`() { + val combined = fixture.getSut() + fixture.scope.fingerprint = listOf("scopeFingerprint") + fixture.isolationScope.fingerprint = listOf("isolationFingerprint") + fixture.globalScope.fingerprint = listOf("globalFingerprint") + + assertEquals(listOf("scopeFingerprint"), combined.fingerprint) + } + + @Test + fun `uses isolation scope fingerprint if current scope does not have one`() { + val combined = fixture.getSut() + fixture.isolationScope.fingerprint = listOf("isolationFingerprint") + fixture.globalScope.fingerprint = listOf("globalFingerprint") + + assertEquals(listOf("isolationFingerprint"), combined.fingerprint) + } + + @Test + fun `uses global scope fingerprint if current and isolation scope do not have one`() { + val combined = fixture.getSut() + fixture.globalScope.fingerprint = listOf("globalFingerprint") + + assertEquals(listOf("globalFingerprint"), combined.fingerprint) + } + + @Test + fun `prefers replay ID from current scope`() { + val combined = fixture.getSut() + fixture.scope.replayId = SentryId("a9118105af4a2d42b4124532cd1065fa") + fixture.isolationScope.replayId = SentryId("e9118105af4a2d42b4124532cd1065fe") + fixture.globalScope.replayId = SentryId("f9118105af4a2d42b4124532cd1065ff") + + assertEquals("a9118105af4a2d42b4124532cd1065fa", combined.replayId.toString()) + } + + @Test + fun `uses isolation scope replay ID if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.replayId = SentryId("e9118105af4a2d42b4124532cd1065fe") + fixture.globalScope.replayId = SentryId("f9118105af4a2d42b4124532cd1065ff") + + assertEquals("e9118105af4a2d42b4124532cd1065fe", combined.replayId.toString()) + } + + @Test + fun `uses global scope replay ID if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.replayId = SentryId("f9118105af4a2d42b4124532cd1065ff") + + assertEquals("f9118105af4a2d42b4124532cd1065ff", combined.replayId.toString()) + } + + @Test + fun `returns empty replay ID if none in any scope`() { + val combined = fixture.getSut() + + assertEquals(SentryId.EMPTY_ID, combined.replayId) + } + + @Test + fun `set replay ID modifies default scope`() { + val combined = fixture.getSut() + combined.replayId = SentryId("b9118105af4a2d42b4124532cd1065fb") + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) + assertEquals("b9118105af4a2d42b4124532cd1065fb", fixture.isolationScope.replayId.toString()) + assertEquals(SentryId.EMPTY_ID, fixture.globalScope.replayId) + } + + private fun createTransaction(name: String, scopes: Scopes? = null): ITransaction { + val scopesToUse = scopes ?: fixture.scopes + return SentryTracer(TransactionContext(name, "op", TracesSamplingDecision(true)), scopesToUse).also { + it.startChild("${name}Span") + } + } + + private fun createAttachment(name: String): Attachment { + return Attachment("a".toByteArray(), name, "image/png", false) + } + + class TestEventProcessor(val orderNumber: Long?) : EventProcessor { + override fun getOrder() = orderNumber + } +} diff --git a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt index c0445efb239..60005935c94 100644 --- a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt @@ -4,7 +4,7 @@ import io.sentry.test.DeferredExecutorService import io.sentry.test.getCtor import io.sentry.test.getProperty import io.sentry.test.injectForField -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.ThreadChecker import org.mockito.kotlin.any import org.mockito.kotlin.atLeast import org.mockito.kotlin.eq @@ -28,12 +28,12 @@ class DefaultTransactionPerformanceCollectorTest { private val className = "io.sentry.DefaultTransactionPerformanceCollector" private val ctorTypes: Array> = arrayOf(SentryOptions::class.java) private val fixture = Fixture() - private val mainThreadChecker = MainThreadChecker.getInstance() + private val threadChecker = ThreadChecker.getInstance() private class Fixture { lateinit var transaction1: ITransaction lateinit var transaction2: ITransaction - val hub: IHub = mock() + val scopes: IScopes = mock() val options = SentryOptions() var mockTimer: Timer? = null val deferredExecutorService = DeferredExecutorService() @@ -47,7 +47,7 @@ class DefaultTransactionPerformanceCollectorTest { } init { - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) } fun getSut(memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, executorService: ISentryExecutorService = deferredExecutorService): TransactionPerformanceCollector { @@ -59,8 +59,8 @@ class DefaultTransactionPerformanceCollectorTest { if (memoryCollector != null) { options.addPerformanceCollector(memoryCollector) } - transaction1 = SentryTracer(TransactionContext("", ""), hub) - transaction2 = SentryTracer(TransactionContext("", ""), hub) + transaction1 = SentryTracer(TransactionContext("", ""), scopes) + transaction2 = SentryTracer(TransactionContext("", ""), scopes) val collector = DefaultTransactionPerformanceCollector(options) val timer: Timer = collector.getProperty("timer") ?: Timer(true) mockTimer = spy(timer) @@ -324,13 +324,13 @@ class DefaultTransactionPerformanceCollectorTest { inner class ThreadCheckerCollector : IPerformanceSnapshotCollector { override fun setup() { - if (mainThreadChecker.isMainThread) { + if (threadChecker.isMainThread) { throw AssertionError("setup() was called in the main thread") } } override fun collect(performanceCollectionData: PerformanceCollectionData) { - if (mainThreadChecker.isMainThread) { + if (threadChecker.isMainThread) { throw AssertionError("collect() was called in the main thread") } } diff --git a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt index e87f4256d58..0507b8499d9 100644 --- a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt @@ -27,7 +27,7 @@ class DirectoryProcessorTest { private class Fixture { - var hub: IHub = mock() + var scopes: IScopes = mock() var envelopeReader: IEnvelopeReader = mock() var serializer: ISerializer = mock() var logger: ILogger = mock() @@ -40,7 +40,7 @@ class DirectoryProcessorTest { fun getSut(isRetryable: Boolean = false, isRateLimitingActive: Boolean = false): OutboxSender { val hintCaptor = argumentCaptor() - whenever(hub.captureEvent(any(), hintCaptor.capture())).then { + whenever(scopes.captureEvent(any(), hintCaptor.capture())).then { HintUtils.runIfHasType( hintCaptor.firstValue, Enqueable::class.java @@ -52,7 +52,7 @@ class DirectoryProcessorTest { val rateLimiter = mock { whenever(mock.isActiveForCategory(any())).thenReturn(true) } - whenever(hub.rateLimiter).thenReturn(rateLimiter) + whenever(scopes.rateLimiter).thenReturn(rateLimiter) } } HintUtils.runIfHasType( @@ -62,7 +62,7 @@ class DirectoryProcessorTest { retryable.isRetry = isRetryable } } - return OutboxSender(hub, envelopeReader, serializer, logger, 500, 30) + return OutboxSender(scopes, envelopeReader, serializer, logger, 500, 30) } } @@ -91,7 +91,7 @@ class DirectoryProcessorTest { whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(event) fixture.getSut().processDirectory(file) - verify(fixture.hub).captureEvent(any(), argWhere { !HintUtils.hasType(it, ApplyScopeData::class.java) }) + verify(fixture.scopes).captureEvent(any(), argWhere { !HintUtils.hasType(it, ApplyScopeData::class.java) }) } @Test @@ -100,7 +100,7 @@ class DirectoryProcessorTest { dir.mkdirs() assertTrue(dir.exists()) // sanity check fixture.getSut().processDirectory(file) - verify(fixture.hub, never()).captureEnvelope(any(), any()) + verify(fixture.scopes, never()).captureEnvelope(any(), any()) } @Test @@ -121,7 +121,7 @@ class DirectoryProcessorTest { sut.processDirectory(file) // should only capture once - verify(fixture.hub).captureEvent(any(), anyOrNull()) + verify(fixture.scopes).captureEvent(any(), anyOrNull()) } @Test @@ -139,7 +139,7 @@ class DirectoryProcessorTest { sut.processDirectory(file) // should only capture once - verify(fixture.hub).captureEvent(any(), anyOrNull()) + verify(fixture.scopes).captureEvent(any(), anyOrNull()) } private fun getTempEnvelope(fileName: String): String { diff --git a/sentry/src/test/java/io/sentry/DisabledQueueTest.kt b/sentry/src/test/java/io/sentry/DisabledQueueTest.kt index 351f87eaff7..aa7484b7c27 100644 --- a/sentry/src/test/java/io/sentry/DisabledQueueTest.kt +++ b/sentry/src/test/java/io/sentry/DisabledQueueTest.kt @@ -5,6 +5,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue class DisabledQueueTest { @@ -22,17 +23,17 @@ class DisabledQueueTest { } @Test - fun `isEmpty returns false when created`() { + fun `isEmpty returns true when created`() { val queue = DisabledQueue() - assertFalse(queue.isEmpty(), "isEmpty should always return false.") + assertTrue(queue.isEmpty(), "isEmpty should always return true.") } @Test - fun `isEmpty always returns false if add function was called`() { + fun `isEmpty always returns true if add function was called`() { val queue = DisabledQueue() queue.add(1) - assertFalse(queue.isEmpty(), "isEmpty should always return false.") + assertTrue(queue.isEmpty(), "isEmpty should always return true.") } @Test diff --git a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt index 6f0ea9cb8a6..d63ee81854f 100644 --- a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt +++ b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt @@ -23,7 +23,7 @@ import kotlin.test.assertFalse class EnvelopeSenderTest { private class Fixture { - var hub: IHub? = mock() + var scopes: IScopes? = mock() var logger: ILogger? = mock() var serializer: ISerializer? = mock() var options = SentryOptions().noFlushTimeout() @@ -35,7 +35,7 @@ class EnvelopeSenderTest { fun getSut(): EnvelopeSender { return EnvelopeSender( - hub!!, + scopes!!, serializer!!, logger!!, options.flushTimeoutMillis, @@ -62,7 +62,7 @@ class EnvelopeSenderTest { val sut = fixture.getSut() sut.processDirectory(File("i don't exist")) verify(fixture.logger)!!.log(eq(SentryLevel.WARNING), eq("Directory '%s' doesn't exist. No cached events to send."), any()) - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) } @Test @@ -72,7 +72,7 @@ class EnvelopeSenderTest { testFile.deleteOnExit() sut.processDirectory(testFile) verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq("Cache dir %s is not a directory."), any()) - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) } @Test @@ -82,11 +82,11 @@ class EnvelopeSenderTest { sut.processDirectory(File(tempDirectory.toUri())) testFile.deleteOnExit() verify(fixture.logger)!!.log(eq(SentryLevel.DEBUG), eq("File '%s' doesn't match extension expected."), any()) - verify(fixture.hub, never())!!.captureEnvelope(any(), anyOrNull()) + verify(fixture.scopes, never())!!.captureEnvelope(any(), anyOrNull()) } @Test - fun `when directory has event files, processDirectory captures with hub`() { + fun `when directory has event files, processDirectory captures with scopes`() { val event = SentryEvent() val envelope = SentryEnvelope.from(fixture.serializer!!, event, null) whenever(fixture.serializer!!.deserializeEnvelope(any())).thenReturn(envelope) @@ -94,7 +94,7 @@ class EnvelopeSenderTest { val testFile = File(Files.createTempFile(tempDirectory, "send-cached-event-test", EnvelopeCache.SUFFIX_ENVELOPE_FILE).toUri()) testFile.deleteOnExit() sut.processDirectory(File(tempDirectory.toUri())) - verify(fixture.hub)!!.captureEnvelope(eq(envelope), any()) + verify(fixture.scopes)!!.captureEnvelope(eq(envelope), any()) } @Test @@ -108,12 +108,12 @@ class EnvelopeSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processFile(testFile, hints) verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached envelope %s"), eq(testFile.absolutePath)) - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) assertFalse(testFile.exists()) } @Test - fun `when hub throws, file gets deleted`() { + fun `when scopes throws, file gets deleted`() { val expected = RuntimeException() whenever(fixture.serializer!!.deserializeEnvelope(any())).doThrow(expected) val sut = fixture.getSut() @@ -121,6 +121,6 @@ class EnvelopeSenderTest { testFile.deleteOnExit() sut.processFile(testFile, Hint()) verify(fixture.logger)!!.log(eq(SentryLevel.ERROR), eq(expected), eq("Failed to capture cached envelope %s"), eq(testFile.absolutePath)) - verifyNoMoreInteractions(fixture.hub) + verifyNoMoreInteractions(fixture.scopes) } } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 04c181b194a..b25f67405cf 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -106,13 +106,6 @@ class ExternalOptionsTest { } } - @Test - fun `creates options with enableTracing using external properties`() { - withPropertiesFile("enable-tracing=true") { - assertEquals(true, it.enableTracing) - } - } - @Test fun `creates options with tracesSampleRate using external properties`() { withPropertiesFile("traces-sample-rate=0.2") { @@ -268,6 +261,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with ignoredTransactions`() { + withPropertiesFile("ignored-transactions=transactionName1,transactionName2") { options -> + assertTrue(options.ignoredTransactions!!.containsAll(listOf("transactionName1", "transactionName2"))) + } + } + @Test fun `creates options with enableBackpressureHandling set to false`() { withPropertiesFile("enable-backpressure-handling=false") { options -> @@ -286,6 +286,48 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with sendDefaultPii set to true`() { + withPropertiesFile("send-default-pii=true") { options -> + assertTrue(options.isSendDefaultPii == true) + } + } + + @Test + fun `creates options with forceInit set to true`() { + withPropertiesFile("force-init=true") { options -> + assertTrue(options.isForceInit == true) + } + } + + @Test + fun `creates options with enableSpotlight set to true`() { + withPropertiesFile("enable-spotlight=true") { options -> + assertTrue(options.isEnableSpotlight == true) + } + } + + @Test + fun `creates options with spotlightConnectionUrl set`() { + withPropertiesFile("spotlight-connection-url=http://local.sentry.io:1234") { options -> + assertEquals("http://local.sentry.io:1234", options.spotlightConnectionUrl) + } + } + + @Test + fun `creates options with globalHubMode set to true`() { + withPropertiesFile("global-hub-mode=true") { options -> + assertTrue(options.isGlobalHubMode == true) + } + } + + @Test + fun `creates options with globalHubMode set to false`() { + withPropertiesFile("global-hub-mode=false") { options -> + assertTrue(options.isGlobalHubMode == false) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 9686250d205..76e79b0e48f 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -2,7 +2,10 @@ package io.sentry import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.test.createSentryClientMock +import io.sentry.test.initForTest import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.reset @@ -13,11 +16,14 @@ import kotlin.test.Test class HubAdapterTest { - val hub: Hub = mock() + val scopes: IScopes = mock() @BeforeTest fun `set up`() { - Sentry.setCurrentHub(hub) + initForTest { + it.dsn = "https://key@localhost/proj" + } + Sentry.setCurrentScopes(scopes) } @AfterTest @@ -27,7 +33,7 @@ class HubAdapterTest { @Test fun `isEnabled calls Hub`() { HubAdapter.getInstance().isEnabled - verify(hub).isEnabled + verify(scopes).isEnabled } @Test fun `captureEvent calls Hub`() { @@ -35,27 +41,27 @@ class HubAdapterTest { val hint = mock() val scopeCallback = mock() HubAdapter.getInstance().captureEvent(event, hint) - verify(hub).captureEvent(eq(event), eq(hint)) + verify(scopes).captureEvent(eq(event), eq(hint)) HubAdapter.getInstance().captureEvent(event, hint, scopeCallback) - verify(hub).captureEvent(eq(event), eq(hint), eq(scopeCallback)) + verify(scopes).captureEvent(eq(event), eq(hint), eq(scopeCallback)) } @Test fun `captureMessage calls Hub`() { val scopeCallback = mock() val sentryLevel = mock() HubAdapter.getInstance().captureMessage("message", sentryLevel) - verify(hub).captureMessage(eq("message"), eq(sentryLevel)) + verify(scopes).captureMessage(eq("message"), eq(sentryLevel)) HubAdapter.getInstance().captureMessage("message", sentryLevel, scopeCallback) - verify(hub).captureMessage(eq("message"), eq(sentryLevel), eq(scopeCallback)) + verify(scopes).captureMessage(eq("message"), eq(sentryLevel), eq(scopeCallback)) } @Test fun `captureEnvelope calls Hub`() { val envelope = mock() val hint = mock() HubAdapter.getInstance().captureEnvelope(envelope, hint) - verify(hub).captureEnvelope(eq(envelope), eq(hint)) + verify(scopes).captureEnvelope(eq(envelope), eq(hint)) } @Test fun `captureException calls Hub`() { @@ -63,145 +69,145 @@ class HubAdapterTest { val hint = mock() val scopeCallback = mock() HubAdapter.getInstance().captureException(throwable, hint) - verify(hub).captureException(eq(throwable), eq(hint)) + verify(scopes).captureException(eq(throwable), eq(hint)) HubAdapter.getInstance().captureException(throwable, hint, scopeCallback) - verify(hub).captureException(eq(throwable), eq(hint), eq(scopeCallback)) + verify(scopes).captureException(eq(throwable), eq(hint), eq(scopeCallback)) } @Test fun `captureUserFeedback calls Hub`() { val userFeedback = mock() HubAdapter.getInstance().captureUserFeedback(userFeedback) - verify(hub).captureUserFeedback(eq(userFeedback)) + verify(scopes).captureUserFeedback(eq(userFeedback)) } @Test fun `captureCheckIn calls Hub`() { val checkIn = mock() HubAdapter.getInstance().captureCheckIn(checkIn) - verify(hub).captureCheckIn(eq(checkIn)) + verify(scopes).captureCheckIn(eq(checkIn)) } @Test fun `startSession calls Hub`() { HubAdapter.getInstance().startSession() - verify(hub).startSession() + verify(scopes).startSession() } @Test fun `endSession calls Hub`() { HubAdapter.getInstance().endSession() - verify(hub).endSession() + verify(scopes).endSession() } @Test fun `close calls Hub`() { HubAdapter.getInstance().close() - verify(hub).close(false) + verify(scopes).close(false) } @Test fun `close with isRestarting true calls Hub with isRestarting false`() { HubAdapter.getInstance().close(true) - verify(hub).close(false) + verify(scopes).close(false) } @Test fun `close with isRestarting false calls Hub with isRestarting false`() { HubAdapter.getInstance().close(false) - verify(hub).close(false) + verify(scopes).close(false) } @Test fun `addBreadcrumb calls Hub`() { val breadcrumb = mock() val hint = mock() HubAdapter.getInstance().addBreadcrumb(breadcrumb, hint) - verify(hub).addBreadcrumb(eq(breadcrumb), eq(hint)) + verify(scopes).addBreadcrumb(eq(breadcrumb), eq(hint)) } @Test fun `setLevel calls Hub`() { val sentryLevel = mock() HubAdapter.getInstance().setLevel(sentryLevel) - verify(hub).setLevel(eq(sentryLevel)) + verify(scopes).setLevel(eq(sentryLevel)) } @Test fun `setTransaction calls Hub`() { HubAdapter.getInstance().setTransaction("transaction") - verify(hub).setTransaction(eq("transaction")) + verify(scopes).setTransaction(eq("transaction")) } @Test fun `setUser calls Hub`() { val user = mock() HubAdapter.getInstance().setUser(user) - verify(hub).setUser(eq(user)) + verify(scopes).setUser(eq(user)) } @Test fun `setFingerprint calls Hub`() { val fingerprint = ArrayList() HubAdapter.getInstance().setFingerprint(fingerprint) - verify(hub).setFingerprint(eq(fingerprint)) + verify(scopes).setFingerprint(eq(fingerprint)) } @Test fun `clearBreadcrumbs calls Hub`() { HubAdapter.getInstance().clearBreadcrumbs() - verify(hub).clearBreadcrumbs() + verify(scopes).clearBreadcrumbs() } @Test fun `setTag calls Hub`() { HubAdapter.getInstance().setTag("key", "value") - verify(hub).setTag(eq("key"), eq("value")) + verify(scopes).setTag(eq("key"), eq("value")) } @Test fun `removeTag calls Hub`() { HubAdapter.getInstance().removeTag("key") - verify(hub).removeTag(eq("key")) + verify(scopes).removeTag(eq("key")) } @Test fun `setExtra calls Hub`() { HubAdapter.getInstance().setExtra("key", "value") - verify(hub).setExtra(eq("key"), eq("value")) + verify(scopes).setExtra(eq("key"), eq("value")) } @Test fun `removeExtra calls Hub`() { HubAdapter.getInstance().removeExtra("key") - verify(hub).removeExtra(eq("key")) + verify(scopes).removeExtra(eq("key")) } @Test fun `getLastEventId calls Hub`() { HubAdapter.getInstance().lastEventId - verify(hub).lastEventId + verify(scopes).lastEventId } @Test fun `pushScope calls Hub`() { HubAdapter.getInstance().pushScope() - verify(hub).pushScope() + verify(scopes).pushScope() } @Test fun `popScope calls Hub`() { HubAdapter.getInstance().popScope() - verify(hub).popScope() + verify(scopes).popScope() } @Test fun `withScope calls Hub`() { val scopeCallback = mock() HubAdapter.getInstance().withScope(scopeCallback) - verify(hub).withScope(eq(scopeCallback)) + verify(scopes).withScope(eq(scopeCallback)) } @Test fun `configureScope calls Hub`() { val scopeCallback = mock() HubAdapter.getInstance().configureScope(scopeCallback) - verify(hub).configureScope(eq(scopeCallback)) + verify(scopes).configureScope(anyOrNull(), eq(scopeCallback)) } @Test fun `bindClient calls Hub`() { - val client = mock() + val client = createSentryClientMock() HubAdapter.getInstance().bindClient(client) - verify(hub).bindClient(eq(client)) + verify(scopes).bindClient(eq(client)) } @Test fun `flush calls Hub`() { HubAdapter.getInstance().flush(1) - verify(hub).flush(eq(1)) + verify(scopes).flush(eq(1)) } @Test fun `clone calls Hub`() { HubAdapter.getInstance().clone() - verify(hub).clone() + verify(scopes).clone() } @Test fun `captureTransaction calls Hub`() { @@ -210,7 +216,7 @@ class HubAdapterTest { val hint = mock() val profilingTraceData = mock() HubAdapter.getInstance().captureTransaction(transaction, traceContext, hint, profilingTraceData) - verify(hub).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) + verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } @Test fun `startTransaction calls Hub`() { @@ -218,48 +224,43 @@ class HubAdapterTest { val samplingContext = mock() val transactionOptions = mock() HubAdapter.getInstance().startTransaction(transactionContext) - verify(hub).startTransaction(eq(transactionContext), any()) + verify(scopes).startTransaction(eq(transactionContext), any()) - reset(hub) + reset(scopes) HubAdapter.getInstance().startTransaction(transactionContext, transactionOptions) - verify(hub).startTransaction(eq(transactionContext), eq(transactionOptions)) - } - - @Test fun `traceHeaders calls Hub`() { - HubAdapter.getInstance().traceHeaders() - verify(hub).traceHeaders() + verify(scopes).startTransaction(eq(transactionContext), eq(transactionOptions)) } @Test fun `setSpanContext calls Hub`() { val throwable = mock() val span = mock() HubAdapter.getInstance().setSpanContext(throwable, span, "transactionName") - verify(hub).setSpanContext(eq(throwable), eq(span), eq("transactionName")) + verify(scopes).setSpanContext(eq(throwable), eq(span), eq("transactionName")) } @Test fun `getSpan calls Hub`() { HubAdapter.getInstance().span - verify(hub).span + verify(scopes).span } @Test fun `getTransaction calls Hub`() { HubAdapter.getInstance().transaction - verify(hub).transaction + verify(scopes).transaction } @Test fun `getOptions calls Hub`() { HubAdapter.getInstance().options - verify(hub).options + verify(scopes).options } @Test fun `isCrashedLastRun calls Hub`() { HubAdapter.getInstance().isCrashedLastRun - verify(hub).isCrashedLastRun + verify(scopes).isCrashedLastRun } @Test fun `reportFullyDisplayed calls Hub`() { HubAdapter.getInstance().reportFullyDisplayed() - verify(hub).reportFullyDisplayed() + verify(scopes).reportFullyDisplayed() } } diff --git a/sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt b/sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt index b28efd2fc4d..ec18b4ed5c2 100644 --- a/sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt +++ b/sentry/src/test/java/io/sentry/JsonObjectReaderTest.kt @@ -205,48 +205,6 @@ class JsonObjectReaderTest { verify(fixture.logger, never()).log(any(), any(), any()) } - // nextMapOfListOrNull - - @Test - fun `returns null for null map list`() { - val jsonString = "{\"metrics_summary\": null}" - val reader = fixture.getSut(jsonString) - reader.beginObject() - reader.nextName() - - assertNull(reader.nextMapOfListOrNull(fixture.logger, Deserializable.Deserializer())) - } - - @Test - fun `returns empty list for map with empty list`() { - val jsonString = "{\"metrics_summary\": { \"metric_a\": [] }}" - val reader = fixture.getSut(jsonString) - reader.beginObject() - reader.nextName() - - val expected = mapOf( - "metric_a" to emptyList() - ) - val actual = reader.nextMapOfListOrNull(fixture.logger, Deserializable.Deserializer()) - assertEquals(expected, actual) - verify(fixture.logger, never()).log(any(), any(), any()) - } - - @Test - fun `returns list for map with one item`() { - val jsonString = "{\"metrics_summary\": { \"metric_a\": [{\"foo\": \"foo\", \"bar\": \"bar\"" + "}]}}" - val reader = fixture.getSut(jsonString) - reader.beginObject() - reader.nextName() - - val expected = mapOf( - "metric_a" to listOf(Deserializable("foo", "bar")) - ) - val actual = reader.nextMapOfListOrNull(fixture.logger, Deserializable.Deserializer()) - assertEquals(expected, actual) - verify(fixture.logger, never()).log(any(), any(), any()) - } - // nextDateOrNull @Test diff --git a/sentry/src/test/java/io/sentry/JsonObjectSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonObjectSerializerTest.kt index cca66f1de23..83634bc0fe0 100644 --- a/sentry/src/test/java/io/sentry/JsonObjectSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonObjectSerializerTest.kt @@ -10,7 +10,6 @@ import java.util.Calendar import java.util.Currency import java.util.Locale import java.util.TimeZone -import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicIntegerArray @@ -240,7 +239,7 @@ internal class JsonObjectSerializerTest { @Test fun `serializing UUID`() { - fixture.getSUT().serialize(fixture.writer, fixture.logger, UUID.fromString("828900a5-15dc-413f-8c17-6ef04d74e074")) + fixture.getSUT().serialize(fixture.writer, fixture.logger, "828900a5-15dc-413f-8c17-6ef04d74e074") verify(fixture.writer).value("828900a5-15dc-413f-8c17-6ef04d74e074") } diff --git a/sentry/src/test/java/io/sentry/JsonReflectionObjectSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonReflectionObjectSerializerTest.kt index 7ea3e2b594f..9a71707ce25 100644 --- a/sentry/src/test/java/io/sentry/JsonReflectionObjectSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonReflectionObjectSerializerTest.kt @@ -7,7 +7,6 @@ import java.net.URI import java.util.Calendar import java.util.Currency import java.util.Locale -import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicIntegerArray import kotlin.test.assertEquals @@ -318,7 +317,7 @@ class JsonReflectionObjectSerializerTest { @Test fun `UUID is serialized`() { - val actual = fixture.getSut().serialize(UUID.fromString("828900a5-15dc-413f-8c17-6ef04d74e074"), fixture.logger) + val actual = fixture.getSut().serialize("828900a5-15dc-413f-8c17-6ef04d74e074", fixture.logger) assertEquals("828900a5-15dc-413f-8c17-6ef04d74e074", actual) } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index 37c18702883..e9d12ff4b17 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -45,7 +45,7 @@ class JsonSerializerTest { private class Fixture { val logger: ILogger = mock() val serializer: ISerializer - val hub = mock() + val scopes = mock() val traceFile = Files.createTempFile("test", "here").toFile() val options = SentryOptions() @@ -53,7 +53,7 @@ class JsonSerializerTest { options.dsn = "https://key@sentry.io/proj" options.setLogger(logger) options.isDebug = true - whenever(hub.options).thenReturn(options) + whenever(scopes.options).thenReturn(options) serializer = JsonSerializer(options) options.setSerializer(serializer) options.setEnvelopeReader(EnvelopeReader(serializer)) @@ -371,6 +371,17 @@ class JsonSerializerTest { assertSessionData(expectedSession) } + @Test + fun `session deserializes 32 character id`() { + val sessionId = "c81d4e2ebcf211e6869b7df92533d2db" + val session = createSessionMockData("c81d4e2ebcf211e6869b7df92533d2db") + val jsonSession = serializeToString(session) + // reversing, so we can assert values and not a json string + val expectedSession = fixture.serializer.deserialize(StringReader(jsonSession), Session::class.java) + + assertSessionData(expectedSession, "c81d4e2ebcf211e6869b7df92533d2db") + } + @Test fun `When deserializing an Envelope, all the values should be set to the SentryEnvelope object`() { val jsonEnvelope = FileFromResources.invoke("envelope_session.txt") @@ -445,15 +456,15 @@ class JsonSerializerTest { @Test fun `serializes trace context`() { - val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", "userId", "segment", "transaction", "0.5", "true", SentryId("3367f5196c494acaae85bbbd535379aa"))) - val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","user_segment":"segment","transaction":"transaction","sample_rate":"0.5","sampled":"true","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" + val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", "userId", "transaction", "0.5", "true", SentryId("3367f5196c494acaae85bbbd535379aa"))) + val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","transaction":"transaction","sample_rate":"0.5","sampled":"true","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" val json = serializeToString(traceContext) assertEquals(expected, json) } @Test - fun `serializes trace context with user having null id and segment`() { - val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", null, null, "transaction", "0.6", "false", SentryId("3367f5196c494acaae85bbbd535379aa"))) + fun `serializes trace context with user having null id`() { + val traceContext = SentryEnvelopeHeader(null, null, TraceContext(SentryId("3367f5196c494acaae85bbbd535379ac"), "key", "release", "environment", null, "transaction", "0.6", "false", SentryId("3367f5196c494acaae85bbbd535379aa"))) val expected = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","transaction":"transaction","sample_rate":"0.6","sampled":"false","replay_id":"3367f5196c494acaae85bbbd535379aa"}}""" val json = serializeToString(traceContext) assertEquals(expected, json) @@ -461,7 +472,7 @@ class JsonSerializerTest { @Test fun `deserializes trace context`() { - val json = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","user_segment":"segment","transaction":"transaction"}}""" + val json = """{"trace":{"trace_id":"3367f5196c494acaae85bbbd535379ac","public_key":"key","release":"release","environment":"environment","user_id":"userId","transaction":"transaction"}}""" val actual = fixture.serializer.deserialize(StringReader(json), SentryEnvelopeHeader::class.java) assertNotNull(actual) { assertNotNull(it.traceContext) { @@ -470,7 +481,6 @@ class JsonSerializerTest { assertEquals("release", it.release) assertEquals("environment", it.environment) assertEquals("userId", it.userId) - assertEquals("segment", it.userSegment) } } } @@ -486,7 +496,6 @@ class JsonSerializerTest { assertEquals("release", it.release) assertEquals("environment", it.environment) assertNull(it.userId) - assertNull(it.userSegment) } } } @@ -837,7 +846,7 @@ class JsonSerializerTest { trace.status = SpanStatus.OK trace.setTag("myTag", "myValue") trace.sampled = true - val tracer = SentryTracer(trace, fixture.hub) + val tracer = SentryTracer(trace, fixture.scopes) tracer.setData("dataKey", "dataValue") val span = tracer.startChild("child") span.finish(SpanStatus.OK) @@ -857,8 +866,6 @@ class JsonSerializerTest { assertNotNull(element["spans"] as List<*>) assertEquals("myValue", (element["tags"] as Map<*, *>)["myTag"] as String) - assertEquals("dataValue", (element["extra"] as Map<*, *>)["dataKey"] as String) - val jsonSpan = (element["spans"] as List<*>)[0] as Map<*, *> assertNotNull(jsonSpan["trace_id"]) assertNotNull(jsonSpan["span_id"]) @@ -869,6 +876,7 @@ class JsonSerializerTest { assertNotNull(jsonSpan["start_timestamp"]) val jsonTrace = (element["contexts"] as Map<*, *>)["trace"] as Map<*, *> + assertEquals("dataValue", (jsonTrace["data"] as Map<*, *>)["dataKey"] as String) assertNotNull(jsonTrace["trace_id"] as String) assertNotNull(jsonTrace["span_id"] as String) assertEquals("http", jsonTrace["op"] as String) @@ -889,7 +897,10 @@ class JsonSerializerTest { "trace_id": "b156a475de54423d9c1571df97ec7eb6", "span_id": "0a53026963414893", "op": "http", - "status": "ok" + "status": "ok", + "data": { + "transactionDataKey": "transactionDataValue" + } }, "custom": { "some-key": "some-value" @@ -930,6 +941,7 @@ class JsonSerializerTest { assertEquals("0a53026963414893", transaction.contexts.trace!!.spanId.toString()) assertEquals("http", transaction.contexts.trace!!.operation) assertNotNull(transaction.contexts["custom"]) + assertEquals("transactionDataValue", transaction.contexts.trace!!.data!!["transactionDataKey"]) assertEquals("some-value", (transaction.contexts["custom"] as Map<*, *>)["some-key"]) assertEquals("extraValue", transaction.getExtra("extraKey")) @@ -1247,9 +1259,9 @@ class JsonSerializerTest { assertEquals(replayRecording, deserializedRecording) } - private fun assertSessionData(expectedSession: Session?) { + private fun assertSessionData(expectedSession: Session?, expectedSessionId: String = "c81d4e2e-bcf2-11e6-869b-7df92533d2db") { assertNotNull(expectedSession) - assertEquals(UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), expectedSession.sessionId) + assertEquals(expectedSessionId, expectedSession.sessionId) assertEquals("123", expectedSession.distinctId) assertTrue(expectedSession.init!!) assertEquals("2020-02-07T14:16:00.000Z", DateUtils.getTimestamp(expectedSession.started!!)) @@ -1279,14 +1291,14 @@ class JsonSerializerTest { private fun generateEmptySentryEvent(date: Date = Date()): SentryEvent = SentryEvent(date) - private fun createSessionMockData(): Session = + private fun createSessionMockData(sessionId: String = "c81d4e2e-bcf2-11e6-869b-7df92533d2db"): Session = Session( Session.State.Ok, DateUtils.getDateTime("2020-02-07T14:16:00.000Z"), DateUtils.getDateTime("2020-02-07T14:16:00.000Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + sessionId, true, 123456.toLong(), 6000.toDouble(), @@ -1321,7 +1333,7 @@ class JsonSerializerTest { status = SpanStatus.OK setTag("myTag", "myValue") } - val tracer = SentryTracer(trace, fixture.hub) + val tracer = SentryTracer(trace, fixture.scopes) val span = tracer.startChild("child") span.setMeasurement("test_measurement", 1, MeasurementUnit.Custom("test")) span.finish(SpanStatus.OK) diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 8881b6d386a..3cb624d380a 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -33,7 +33,7 @@ class MainEventProcessorTest { dist = "dist" sdkVersion = SdkVersion("test", "1.2.3") } - val hub = mock() + val scopes = mock() val getLocalhost = mock() lateinit var sentryTracer: SentryTracer private val hostnameCacheMock = Mockito.mockStatic(HostnameCache::class.java) @@ -72,8 +72,8 @@ class MainEventProcessorTest { } host } - whenever(hub.options).thenReturn(sentryOptions) - sentryTracer = SentryTracer(TransactionContext("", ""), hub) + whenever(scopes.options).thenReturn(sentryOptions) + sentryTracer = SentryTracer(TransactionContext("", ""), scopes) val hostnameCache = HostnameCache(hostnameCacheDuration) { getLocalhost } hostnameCacheMock.`when` { HostnameCache.getInstance() }.thenReturn(hostnameCache) diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt deleted file mode 100644 index 01948114de8..00000000000 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ /dev/null @@ -1,532 +0,0 @@ -package io.sentry - -import io.sentry.SentryOptions.BeforeEmitMetricCallback -import io.sentry.metrics.IMetricsClient -import io.sentry.metrics.LocalMetricsAggregator -import io.sentry.metrics.MetricType -import io.sentry.metrics.MetricsHelper -import io.sentry.metrics.MetricsHelperTest -import io.sentry.test.DeferredExecutorService -import org.mockito.kotlin.any -import org.mockito.kotlin.check -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import java.util.concurrent.TimeUnit -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class MetricsAggregatorTest { - - private class Fixture { - val client = mock() - val logger = mock() - val dateProvider = SentryDateProvider { - SentryLongDate(TimeUnit.MILLISECONDS.toNanos(currentTimeMillis)) - } - var currentTimeMillis: Long = 0 - var executorService = DeferredExecutorService() - - fun getSut( - maxWeight: Int = MetricsHelper.MAX_TOTAL_WEIGHT, - beforeEmitMetricCallback: BeforeEmitMetricCallback? = null - ): MetricsAggregator { - return MetricsAggregator( - client, - logger, - dateProvider, - maxWeight, - beforeEmitMetricCallback, - executorService - ) - } - } - - private val fixture = Fixture() - - @BeforeTest - fun setup() { - MetricsHelper.setFlushShiftMs(0) - } - - @Test - fun `flush is a no-op when there's nothing to flush`() { - val aggregator = fixture.getSut() - - // when no metrics are collected - - // then flush does nothing - aggregator.flush(false) - - verify(fixture.client, never()).captureMetrics(any()) - } - - @Test - fun `flush performs a flush when needed`() { - val aggregator = fixture.getSut() - - // when a metric is emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment("key", 1.0, null, null, 20_001, null) - - // then flush does nothing because there's no data inside the flush interval - aggregator.flush(false) - verify(fixture.client, never()).captureMetrics(any()) - - // as times moves on - fixture.currentTimeMillis = 30_000 - - // the metric should be flushed - aggregator.flush(false) - verify(fixture.client).captureMetrics(any()) - } - - @Test - fun `force flush performs a flushing`() { - val aggregator = fixture.getSut() - // when a metric is emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment("key", 1.0, null, null, 20_001, null) - - // then force flush flushes the metric - aggregator.flush(true) - verify(fixture.client).captureMetrics(any()) - } - - @Test - fun `same metrics are aggregated when in same bucket`() { - val aggregator = fixture.getSut() - - fixture.currentTimeMillis = 20_000 - - aggregator.increment( - "name", - 1.0, - MeasurementUnit.Custom("apples"), - mapOf("a" to "b"), - 20_001, - null - ) - aggregator.increment( - "name", - 1.0, - MeasurementUnit.Custom("apples"), - mapOf("a" to "b"), - 25_001, - null - ) - - // then flush does nothing because there's no data inside the flush interval - aggregator.flush(true) - - verify(fixture.client).captureMetrics( - check { - val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) - assertEquals(1, metrics.size) - assertEquals( - MetricsHelperTest.Companion.StatsDMetric( - 20, - "name", - "apples", - "c", - listOf("2.0"), - mapOf("a" to "b") - ), - metrics[0] - ) - } - ) - } - - @Test - fun `different metrics are not aggregated when in same bucket`() { - val aggregator = fixture.getSut() - - // when different metrics are emitted in the same bucket - fixture.currentTimeMillis = 20_000 - aggregator.distribution( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit1"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit1"), - mapOf("key1" to "value0"), - 20_001, - null - ) - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit1"), - mapOf("key1" to "value1"), - 20_001, - null - ) - - aggregator.flush(true) - - // then all of them are emitted separately - verify(fixture.client).captureMetrics( - check { - val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) - assertEquals(5, metrics.size) - } - ) - } - - @Test - fun `once the aggregator is closed, emissions are ignored`() { - val aggregator = fixture.getSut() - - // when aggregator is closed - aggregator.close() - - // and a metric is emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - - // then the metric is never captured - aggregator.flush(true) - verify(fixture.client, never()).captureMetrics(any()) - } - - @Test - fun `all metric types can be emitted`() { - val aggregator = fixture.getSut() - - fixture.currentTimeMillis = 20_000 - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.distribution( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.set( - "name0-string", - "Hello", - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.set( - "name0-int", - 1234, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - aggregator.gauge( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - - aggregator.flush(true) - verify(fixture.client).captureMetrics( - check { - val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) - assertEquals(5, metrics.size) - } - ) - } - - @Test - fun `flushing gets scheduled and captures metrics`() { - val aggregator = fixture.getSut() - - // when nothing happened so far - // then no flushing is scheduled - assertFalse(fixture.executorService.hasScheduledRunnables()) - - // when a metric gets emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment( - "name0", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - - // then a flush is scheduled - assertTrue(fixture.executorService.hasScheduledRunnables()) - - // flush is executed, but there are other metric to capture and it's scheduled again - fixture.executorService.runAll() - verify(fixture.client, never()).captureMetrics(any()) - assertTrue(fixture.executorService.hasScheduledRunnables()) - - // after the flush is executed, the metric is captured - fixture.currentTimeMillis = 31_000 - fixture.executorService.runAll() - verify(fixture.client).captureMetrics(any()) - - // there is no other metric to capture, so flush is not scheduled again - assertFalse(fixture.executorService.hasScheduledRunnables()) - - // once another metric is emitted - aggregator.increment( - "name1", - 1.0, - MeasurementUnit.Custom("unit0"), - mapOf("key0" to "value0"), - 20_001, - null - ) - - // then flush should be scheduled again - assertTrue(fixture.executorService.hasScheduledRunnables()) - } - - @Test - fun `metric emits get forwarded to local aggregator`() { - val aggregator = fixture.getSut() - - val localAggregator = mock() - - // when a metric gets emitted - val type = MetricType.Counter - val key = "name0" - val value = 4.0 - val unit = MeasurementUnit.Custom("unit0") - val tags = mapOf("key0" to "value0") - val timestamp = 20_001L - - aggregator.increment( - key, - value, - unit, - tags, - timestamp, - localAggregator - ) - - verify(localAggregator).add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags), - type, - key, - value, - unit, - tags - ) - } - - @Test - fun `a set metric forwards a value of 1 to the local aggregator`() { - val aggregator = fixture.getSut() - - val localAggregator = mock() - - // when a new set metric gets emitted - val type = MetricType.Set - val key = "name0" - val value = 1235 - val unit = MeasurementUnit.Custom("unit0") - val tags = mapOf("key0" to "value0") - val timestamp = 20_001L - - aggregator.set( - key, - value, - unit, - tags, - timestamp, - localAggregator - ) - - // then the local aggregator receives a value of 1 - verify(localAggregator).add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags), - type, - key, - 1.0, - unit, - tags - ) - - // if the same set metric is emitted again - aggregator.set( - key, - value, - unit, - tags, - timestamp, - localAggregator - ) - - // then the local aggregator receives a value of 0 - verify(localAggregator).add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags), - type, - key, - 0.0, - unit, - tags - ) - } - - fun `weight is considered for force flushing`() { - // weight is determined by number of buckets + weight of metrics - val aggregator = fixture.getSut(5) - - // when 3 values are emitted - for (i in 0 until 3) { - aggregator.distribution( - "name", - i.toDouble(), - null, - null, - fixture.currentTimeMillis, - null - ) - } - // no metrics are captured by the client - fixture.executorService.runAll() - verify(fixture.client, never()).captureMetrics(any()) - - // once we have 4 values and one bucket = weight of 5 - aggregator.distribution( - "name", - 10.0, - null, - null, - fixture.currentTimeMillis, - null - ) - // then flush without force still captures all metrics - fixture.executorService.runAll() - verify(fixture.client).captureMetrics(any()) - } - - @Test - fun `flushing is immediately scheduled if add operations causes too much weight`() { - fixture.executorService = mock() - val aggregator = fixture.getSut(1) - - verify(fixture.executorService, never()).schedule(any(), any()) - - // when 1 value is emitted - aggregator.distribution( - "name", - 1.0, - null, - null, - fixture.currentTimeMillis, - null - ) - - // flush is immediately scheduled - verify(fixture.executorService).schedule(any(), eq(0)) - } - - @Test - fun `flushing is deferred scheduled if add operations does not cause too much weight`() { - fixture.executorService = mock() - val aggregator = fixture.getSut(10) - - // when 1 value is emitted - aggregator.distribution( - "name", - 1.0, - null, - null, - fixture.currentTimeMillis, - null - ) - - // flush is scheduled for later - verify(fixture.executorService).schedule(any(), eq(MetricsHelper.FLUSHER_SLEEP_TIME_MS)) - } - - @Test - fun `key and tags are passed down to beforeEmitMetricCallback`() { - var lastKey: String? = null - var lastTags: Map? = null - val aggregator = fixture.getSut(beforeEmitMetricCallback = { key, tags -> - lastKey = key - lastTags = tags - return@getSut false - }) - - val key = "metric-key" - val tags = mapOf( - "tag-key" to "tag-value" - ) - aggregator.increment(key, 1.0, null, tags, 20_001, null) - assertEquals(key, lastKey) - assertEquals(tags, lastTags) - } - - @Test - fun `if before emit callback returns true, metric is emitted`() { - val aggregator = fixture.getSut(beforeEmitMetricCallback = { key, tags -> true }) - aggregator.increment("key", 1.0, null, null, 20_001, null) - aggregator.flush(true) - verify(fixture.client).captureMetrics(any()) - } - - @Test - fun `if before emit callback returns false, metric is not emitted`() { - val aggregator = fixture.getSut(beforeEmitMetricCallback = { key, tags -> false }) - aggregator.increment("key", 1.0, null, null, 20_001, null) - aggregator.flush(true) - verify(fixture.client, never()).captureMetrics(any()) - } - - @Test - fun `if before emit throws, metric is emitted`() { - val aggregator = fixture.getSut(beforeEmitMetricCallback = { key, tags -> throw RuntimeException() }) - aggregator.increment("key", 1.0, null, null, 20_001, null) - aggregator.flush(true) - verify(fixture.client).captureMetrics(any()) - } -} diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index dbbfb4b4f1e..f20257482d8 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -6,7 +6,6 @@ import org.mockito.kotlin.verify import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame @@ -71,7 +70,9 @@ class NoOpHubTest { } @Test - fun `pushScope is no op`() = sut.pushScope() + fun `pushScope is no op`() { + sut.pushScope() + } @Test fun `popScope is no op`() = sut.popScope() @@ -82,11 +83,6 @@ class NoOpHubTest { @Test fun `clone returns the same instance`() = assertSame(NoOpHub.getInstance(), sut.clone()) - @Test - fun `traceHeaders is not null`() { - assertNotNull(sut.traceHeaders()) - } - @Test fun `getSpan returns null`() { assertNull(sut.span) diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index ab50e054d0b..8a1850e7ddc 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -5,7 +5,7 @@ import io.sentry.hints.Retryable import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.util.HintUtils -import io.sentry.util.thread.NoOpMainThreadChecker +import io.sentry.util.thread.NoOpThreadChecker import org.mockito.kotlin.any import org.mockito.kotlin.argWhere import org.mockito.kotlin.check @@ -20,7 +20,6 @@ import java.io.FileNotFoundException import java.nio.file.Files import java.nio.file.Paths import java.util.Date -import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -30,7 +29,7 @@ class OutboxSenderTest { private class Fixture { val options = mock() - val hub = mock() + val scopes = mock() var envelopeReader = mock() val serializer = mock() val logger = mock() @@ -38,12 +37,12 @@ class OutboxSenderTest { init { whenever(options.dsn).thenReturn("https://key@sentry.io/proj") whenever(options.dateProvider).thenReturn(SentryNanotimeDateProvider()) - whenever(options.mainThreadChecker).thenReturn(NoOpMainThreadChecker.getInstance()) - whenever(hub.options).thenReturn(this.options) + whenever(options.threadChecker).thenReturn(NoOpThreadChecker.getInstance()) + whenever(scopes.options).thenReturn(this.options) } fun getSut(): OutboxSender { - return OutboxSender(hub, envelopeReader, serializer, logger, 15000, 30) + return OutboxSender(scopes, envelopeReader, serializer, logger, 15000, 30) } } @@ -74,7 +73,7 @@ class OutboxSenderTest { @Test fun `when parser is EnvelopeReader and serializer returns SentryEvent, event captured, file is deleted `() { fixture.envelopeReader = EnvelopeReader(JsonSerializer(fixture.options)) - val expected = SentryEvent(SentryId(UUID.fromString("9ec79c33-ec99-42ab-8353-589fcb2e04dc")), Date()) + val expected = SentryEvent(SentryId("9ec79c33-ec99-42ab-8353-589fcb2e04dc"), Date()) whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(expected) val sut = fixture.getSut() val path = getTempEnvelope() @@ -83,7 +82,7 @@ class OutboxSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processEnvelopeFile(path, hints) - verify(fixture.hub).captureEvent(eq(expected), any()) + verify(fixture.scopes).captureEvent(eq(expected), any()) assertFalse(File(path).exists()) // Additionally make sure we have no errors logged verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) @@ -94,7 +93,7 @@ class OutboxSenderTest { fun `when parser is EnvelopeReader and serializer return SentryTransaction, transaction captured, transactions sampled, file is deleted`() { fixture.envelopeReader = EnvelopeReader(JsonSerializer(fixture.options)) whenever(fixture.options.maxSpans).thenReturn(1000) - whenever(fixture.hub.options).thenReturn(fixture.options) + whenever(fixture.scopes.options).thenReturn(fixture.options) whenever(fixture.options.transactionProfiler).thenReturn(NoOpTransactionProfiler.getInstance()) val transactionContext = TransactionContext("fixture-name", "http") @@ -102,7 +101,7 @@ class OutboxSenderTest { transactionContext.status = SpanStatus.OK transactionContext.setTag("fixture-tag", "fixture-value") - val sentryTracer = SentryTracer(transactionContext, fixture.hub) + val sentryTracer = SentryTracer(transactionContext, fixture.scopes) val span = sentryTracer.startChild("child") span.finish(SpanStatus.OK) sentryTracer.finish() @@ -120,7 +119,7 @@ class OutboxSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processEnvelopeFile(path, hints) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(expected, it) assertTrue(it.isSampled) @@ -139,7 +138,7 @@ class OutboxSenderTest { fun `restores sampleRate`() { fixture.envelopeReader = EnvelopeReader(JsonSerializer(fixture.options)) whenever(fixture.options.maxSpans).thenReturn(1000) - whenever(fixture.hub.options).thenReturn(fixture.options) + whenever(fixture.scopes.options).thenReturn(fixture.options) whenever(fixture.options.transactionProfiler).thenReturn(NoOpTransactionProfiler.getInstance()) val transactionContext = TransactionContext("fixture-name", "http") @@ -148,7 +147,7 @@ class OutboxSenderTest { transactionContext.setTag("fixture-tag", "fixture-value") transactionContext.samplingDecision = TracesSamplingDecision(true, 0.00000021) - val sentryTracer = SentryTracer(transactionContext, fixture.hub) + val sentryTracer = SentryTracer(transactionContext, fixture.scopes) val span = sentryTracer.startChild("child") span.finish(SpanStatus.OK) sentryTracer.finish() @@ -166,7 +165,7 @@ class OutboxSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processEnvelopeFile(path, hints) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(expected, it) assertTrue(it.isSampled) @@ -180,7 +179,6 @@ class OutboxSenderTest { assertEquals("1.0-beta.1", it.release) assertEquals("prod", it.environment) assertEquals("usr1", it.userId) - assertEquals("pro", it.userSegment) assertEquals("tx1", it.transaction) }, any() @@ -207,7 +205,7 @@ class OutboxSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processEnvelopeFile(path, hints) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) assertFalse(File(path).exists()) // Additionally make sure we have no errors logged verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) @@ -225,7 +223,7 @@ class OutboxSenderTest { val hints = HintUtils.createWithTypeCheckHint(mock()) sut.processEnvelopeFile(path, hints) - verify(fixture.hub).captureEnvelope(any(), any()) + verify(fixture.scopes).captureEnvelope(any(), any()) assertFalse(File(path).exists()) // Additionally make sure we have no errors logged verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any(), any()) @@ -245,7 +243,7 @@ class OutboxSenderTest { // Additionally make sure we have no errors logged verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.hub, never()).captureEvent(any()) + verify(fixture.scopes, never()).captureEvent(any()) assertFalse(File(path).exists()) } @@ -263,7 +261,7 @@ class OutboxSenderTest { // Additionally make sure we have no errors logged verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) - verify(fixture.hub, never()).captureEvent(any()) + verify(fixture.scopes, never()).captureEvent(any()) assertFalse(File(path).exists()) } diff --git a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt index 239e90905ec..87aa5e67152 100644 --- a/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt +++ b/sentry/src/test/java/io/sentry/PreviousSessionFinalizerTest.kt @@ -21,7 +21,7 @@ class PreviousSessionFinalizerTest { class Fixture { val options = SentryOptions() - val hub = mock() + val scopes = mock() val logger = mock() lateinit var sessionFile: File @@ -61,7 +61,7 @@ class PreviousSessionFinalizerTest { nativeCrashMarker.writeText(nativeCrashTimestamp.toString()) } } - return PreviousSessionFinalizer(options, hub) + return PreviousSessionFinalizer(options, scopes) } fun sessionFromEnvelope(envelope: SentryEnvelope): Session { @@ -80,7 +80,7 @@ class PreviousSessionFinalizerTest { val finalizer = fixture.getSut(null) finalizer.run() - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) } @Test @@ -88,7 +88,7 @@ class PreviousSessionFinalizerTest { val finalizer = fixture.getSut(tmpDir, sessionFileExists = false) finalizer.run() - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) } @Test @@ -96,7 +96,7 @@ class PreviousSessionFinalizerTest { val finalizer = fixture.getSut(tmpDir, sessionFileExists = true, session = null) finalizer.run() - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) } @Test @@ -107,7 +107,7 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope( + verify(fixture.scopes).captureEnvelope( argThat { val session = fixture.sessionFromEnvelope(this) session.release == "io.sentry.sample@1.0" && @@ -133,7 +133,7 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope( + verify(fixture.scopes).captureEnvelope( argThat { val session = fixture.sessionFromEnvelope(this) session.release == "io.sentry.sample@1.0" && @@ -156,7 +156,7 @@ class PreviousSessionFinalizerTest { ) finalizer.run() - verify(fixture.hub).captureEnvelope( + verify(fixture.scopes).captureEnvelope( argThat { val session = fixture.sessionFromEnvelope(this) session.release == "io.sentry.sample@1.0" && @@ -170,7 +170,7 @@ class PreviousSessionFinalizerTest { val finalizer = fixture.getSut(tmpDir, sessionFileExists = true) finalizer.run() - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) assertFalse(fixture.sessionFile.exists()) } @@ -189,7 +189,7 @@ class PreviousSessionFinalizerTest { argThat { startsWith("Timed out waiting to flush previous session to its own file in session finalizer.") }, any() ) - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) } @Test @@ -202,6 +202,6 @@ class PreviousSessionFinalizerTest { argThat { startsWith("Timed out waiting to flush previous session to its own file in session finalizer.") }, any() ) - verify(fixture.hub, never()).captureEnvelope(any()) + verify(fixture.scopes, never()).captureEnvelope(any()) } } diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index a0f54d5205a..b8025735e8a 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -115,7 +115,7 @@ class ScopeTest { scope.setExtra("extra", "extra") val transaction = - SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + SentryTracer(TransactionContext("transaction-name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction val attachment = Attachment("path/log.txt") @@ -193,7 +193,7 @@ class ScopeTest { scope.setTransaction( SentryTracer( TransactionContext("newTransaction", "op"), - NoOpHub.getInstance() + NoOpScopes.getInstance() ) ) @@ -266,7 +266,7 @@ class ScopeTest { fun `clear scope resets scope to default state`() { val scope = Scope(SentryOptions()) scope.level = SentryLevel.WARNING - scope.setTransaction(SentryTracer(TransactionContext("", "op"), NoOpHub.getInstance())) + scope.setTransaction(SentryTracer(TransactionContext("", "op"), NoOpScopes.getInstance())) scope.user = User() scope.request = Request() scope.fingerprint = mutableListOf("finger") @@ -838,7 +838,7 @@ class ScopeTest { @Test fun `Scope getTransaction returns the transaction if there is no active span`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction assertEquals(transaction, scope.span) } @@ -846,7 +846,7 @@ class ScopeTest { @Test fun `Scope getTransaction returns the current span if there is an unfinished span`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction val span = transaction.startChild("op") assertEquals(span, scope.span) @@ -855,7 +855,7 @@ class ScopeTest { @Test fun `Scope getTransaction returns the current span if there is a finished span`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction val span = transaction.startChild("op") span.finish() @@ -865,7 +865,7 @@ class ScopeTest { @Test fun `Scope getTransaction returns the latest span if there is a list of active span`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction val span = transaction.startChild("op") val innerSpan = span.startChild("op") @@ -875,7 +875,7 @@ class ScopeTest { @Test fun `Scope setTransaction sets transaction name`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction scope.setTransaction("new-name") assertNotNull(scope.transaction) { @@ -887,7 +887,7 @@ class ScopeTest { @Test fun `Scope setTransaction with null does not clear transaction`() { val scope = Scope(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), NoOpHub.getInstance()) + val transaction = SentryTracer(TransactionContext("name", "op"), NoOpScopes.getInstance()) scope.transaction = transaction scope.callMethod("setTransaction", String::class.java, null) assertNotNull(scope.transaction) @@ -952,7 +952,7 @@ class ScopeTest { fun `when transaction is started, sets transaction name on the transaction object`() { val scope = Scope(SentryOptions()) val sentryTransaction = - SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + SentryTracer(TransactionContext("transaction-name", "op"), NoOpScopes.getInstance()) scope.transaction = sentryTransaction assertEquals("transaction-name", scope.transactionName) scope.setTransaction("new-name") @@ -966,7 +966,7 @@ class ScopeTest { val scope = Scope(SentryOptions()) scope.setTransaction("transaction-a") val sentryTransaction = - SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + SentryTracer(TransactionContext("transaction-name", "op"), NoOpScopes.getInstance()) scope.setTransaction(sentryTransaction) assertEquals("transaction-name", scope.transactionName) scope.clearTransaction() @@ -977,7 +977,7 @@ class ScopeTest { fun `withTransaction returns the current Transaction bound to the Scope`() { val scope = Scope(SentryOptions()) val sentryTransaction = - SentryTracer(TransactionContext("transaction-name", "op"), NoOpHub.getInstance()) + SentryTracer(TransactionContext("transaction-name", "op"), NoOpScopes.getInstance()) scope.setTransaction(sentryTransaction) scope.withTransaction { diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt new file mode 100644 index 00000000000..ea274d438b0 --- /dev/null +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -0,0 +1,266 @@ +package io.sentry + +import io.sentry.protocol.SentryTransaction +import io.sentry.protocol.User +import io.sentry.test.createSentryClientMock +import io.sentry.test.initForTest +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ScopesAdapterTest { + + val scopes: IScopes = mock() + + @BeforeTest + fun `set up`() { + initForTest { + it.dsn = "https://key@localhost/proj" + } + Sentry.setCurrentScopes(scopes) + } + + @AfterTest + fun shutdown() { + Sentry.close() + } + + @Test fun `isEnabled calls Scopes`() { + ScopesAdapter.getInstance().isEnabled + verify(scopes).isEnabled + } + + @Test fun `captureEvent calls Scopes`() { + val event = mock() + val hint = mock() + val scopeCallback = mock() + ScopesAdapter.getInstance().captureEvent(event, hint) + verify(scopes).captureEvent(eq(event), eq(hint)) + + ScopesAdapter.getInstance().captureEvent(event, hint, scopeCallback) + verify(scopes).captureEvent(eq(event), eq(hint), eq(scopeCallback)) + } + + @Test fun `captureMessage calls Scopes`() { + val scopeCallback = mock() + val sentryLevel = mock() + ScopesAdapter.getInstance().captureMessage("message", sentryLevel) + verify(scopes).captureMessage(eq("message"), eq(sentryLevel)) + + ScopesAdapter.getInstance().captureMessage("message", sentryLevel, scopeCallback) + verify(scopes).captureMessage(eq("message"), eq(sentryLevel), eq(scopeCallback)) + } + + @Test fun `captureEnvelope calls Scopes`() { + val envelope = mock() + val hint = mock() + ScopesAdapter.getInstance().captureEnvelope(envelope, hint) + verify(scopes).captureEnvelope(eq(envelope), eq(hint)) + } + + @Test fun `captureException calls Scopes`() { + val throwable = mock() + val hint = mock() + val scopeCallback = mock() + ScopesAdapter.getInstance().captureException(throwable, hint) + verify(scopes).captureException(eq(throwable), eq(hint)) + + ScopesAdapter.getInstance().captureException(throwable, hint, scopeCallback) + verify(scopes).captureException(eq(throwable), eq(hint), eq(scopeCallback)) + } + + @Test fun `captureUserFeedback calls Scopes`() { + val userFeedback = mock() + ScopesAdapter.getInstance().captureUserFeedback(userFeedback) + verify(scopes).captureUserFeedback(eq(userFeedback)) + } + + @Test fun `captureCheckIn calls Scopes`() { + val checkIn = mock() + ScopesAdapter.getInstance().captureCheckIn(checkIn) + verify(scopes).captureCheckIn(eq(checkIn)) + } + + @Test fun `startSession calls Scopes`() { + ScopesAdapter.getInstance().startSession() + verify(scopes).startSession() + } + + @Test fun `endSession calls Scopes`() { + ScopesAdapter.getInstance().endSession() + verify(scopes).endSession() + } + + @Test fun `close calls Scopes`() { + ScopesAdapter.getInstance().close() + verify(scopes).close(false) + } + + @Test fun `close with isRestarting true calls Scopes with isRestarting false`() { + ScopesAdapter.getInstance().close(true) + verify(scopes).close(false) + } + + @Test fun `close with isRestarting false calls Scopes with isRestarting false`() { + ScopesAdapter.getInstance().close(false) + verify(scopes).close(false) + } + + @Test fun `addBreadcrumb calls Scopes`() { + val breadcrumb = mock() + val hint = mock() + ScopesAdapter.getInstance().addBreadcrumb(breadcrumb, hint) + verify(scopes).addBreadcrumb(eq(breadcrumb), eq(hint)) + } + + @Test fun `setLevel calls Scopes`() { + val sentryLevel = mock() + ScopesAdapter.getInstance().setLevel(sentryLevel) + verify(scopes).setLevel(eq(sentryLevel)) + } + + @Test fun `setTransaction calls Scopes`() { + ScopesAdapter.getInstance().setTransaction("transaction") + verify(scopes).setTransaction(eq("transaction")) + } + + @Test fun `setUser calls Scopes`() { + val user = mock() + ScopesAdapter.getInstance().setUser(user) + verify(scopes).setUser(eq(user)) + } + + @Test fun `setFingerprint calls Scopes`() { + val fingerprint = ArrayList() + ScopesAdapter.getInstance().setFingerprint(fingerprint) + verify(scopes).setFingerprint(eq(fingerprint)) + } + + @Test fun `clearBreadcrumbs calls Scopes`() { + ScopesAdapter.getInstance().clearBreadcrumbs() + verify(scopes).clearBreadcrumbs() + } + + @Test fun `setTag calls Scopes`() { + ScopesAdapter.getInstance().setTag("key", "value") + verify(scopes).setTag(eq("key"), eq("value")) + } + + @Test fun `removeTag calls Scopes`() { + ScopesAdapter.getInstance().removeTag("key") + verify(scopes).removeTag(eq("key")) + } + + @Test fun `setExtra calls Scopes`() { + ScopesAdapter.getInstance().setExtra("key", "value") + verify(scopes).setExtra(eq("key"), eq("value")) + } + + @Test fun `removeExtra calls Scopes`() { + ScopesAdapter.getInstance().removeExtra("key") + verify(scopes).removeExtra(eq("key")) + } + + @Test fun `getLastEventId calls Scopes`() { + ScopesAdapter.getInstance().lastEventId + verify(scopes).lastEventId + } + + @Test fun `pushScope calls Scopes`() { + ScopesAdapter.getInstance().pushScope() + verify(scopes).pushScope() + } + + @Test fun `popScope calls Scopes`() { + ScopesAdapter.getInstance().popScope() + verify(scopes).popScope() + } + + @Test fun `withScope calls Scopes`() { + val scopeCallback = mock() + ScopesAdapter.getInstance().withScope(scopeCallback) + verify(scopes).withScope(eq(scopeCallback)) + } + + @Test fun `configureScope calls Scopes`() { + val scopeCallback = mock() + ScopesAdapter.getInstance().configureScope(scopeCallback) + verify(scopes).configureScope(anyOrNull(), eq(scopeCallback)) + } + + @Test fun `bindClient calls Scopes`() { + val client = createSentryClientMock() + ScopesAdapter.getInstance().bindClient(client) + verify(scopes).bindClient(eq(client)) + } + + @Test fun `flush calls Scopes`() { + ScopesAdapter.getInstance().flush(1) + verify(scopes).flush(eq(1)) + } + + @Test fun `clone calls Scopes`() { + ScopesAdapter.getInstance().clone() + verify(scopes).clone() + } + + @Test fun `captureTransaction calls Scopes`() { + val transaction = mock() + val traceContext = mock() + val hint = mock() + val profilingTraceData = mock() + ScopesAdapter.getInstance().captureTransaction(transaction, traceContext, hint, profilingTraceData) + verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) + } + + @Test fun `startTransaction calls Scopes`() { + val transactionContext = mock() + val samplingContext = mock() + val transactionOptions = mock() + ScopesAdapter.getInstance().startTransaction(transactionContext) + verify(scopes).startTransaction(eq(transactionContext), any()) + + reset(scopes) + + ScopesAdapter.getInstance().startTransaction(transactionContext, transactionOptions) + verify(scopes).startTransaction(eq(transactionContext), eq(transactionOptions)) + } + + @Test fun `setSpanContext calls Scopes`() { + val throwable = mock() + val span = mock() + ScopesAdapter.getInstance().setSpanContext(throwable, span, "transactionName") + verify(scopes).setSpanContext(eq(throwable), eq(span), eq("transactionName")) + } + + @Test fun `getSpan calls Scopes`() { + ScopesAdapter.getInstance().span + verify(scopes).span + } + + @Test fun `getTransaction calls Scopes`() { + ScopesAdapter.getInstance().transaction + verify(scopes).transaction + } + + @Test fun `getOptions calls Scopes`() { + ScopesAdapter.getInstance().options + verify(scopes).options + } + + @Test fun `isCrashedLastRun calls Scopes`() { + ScopesAdapter.getInstance().isCrashedLastRun + verify(scopes).isCrashedLastRun + } + + @Test fun `reportFullyDisplayed calls Scopes`() { + ScopesAdapter.getInstance().reportFullyDisplayed() + verify(scopes).reportFullyDisplayed() + } +} diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt similarity index 73% rename from sentry/src/test/java/io/sentry/HubTest.kt rename to sentry/src/test/java/io/sentry/ScopesTest.kt index 50f996ccdd7..fdbbf61b058 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -12,8 +12,12 @@ import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.test.DeferredExecutorService import io.sentry.test.callMethod +import io.sentry.test.createSentryClientMock +import io.sentry.test.createTestScopes +import io.sentry.test.initForTest import io.sentry.util.HintUtils import io.sentry.util.StringUtils +import junit.framework.TestCase.assertSame import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere @@ -24,6 +28,7 @@ import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.reset import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -46,7 +51,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.test.fail -class HubTest { +class ScopesTest { private lateinit var file: File private lateinit var profilingTraceFile: File @@ -56,6 +61,7 @@ class HubTest { file = Files.createTempDirectory("sentry-disk-cache-test").toAbsolutePath().toFile() profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() profilingTraceFile.writeText("sampledProfile") + SentryCrashLastRunState.getInstance().reset() } @AfterTest @@ -63,59 +69,107 @@ class HubTest { file.deleteRecursively() profilingTraceFile.delete() Sentry.close() + SentryCrashLastRunState.getInstance().reset() + } + + private fun createScopes(options: SentryOptions): Scopes { + return createTestScopes(options).also { + it.bindClient(SentryClient(options)) + } } @Test fun `when no dsn available, ctor throws illegal arg`() { - val ex = assertFailsWith { Hub(SentryOptions()) } - assertEquals("Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available.", ex.message) + val ex = assertFailsWith { + val options = SentryOptions() + val scopeToUse = Scope(options) + val isolationScopeToUse = Scope(options) + val globalScopeToUse = Scope(options) + Scopes(scopeToUse, isolationScopeToUse, globalScopeToUse, "test") + } + assertEquals("Scopes requires a DSN to be instantiated. Considering using the NoOpScopes if no DSN is available.", ex.message) } @Test - fun `when hub is cloned, integrations are not registered`() { + fun `when isolation scope is forked, integrations are not registered`() { val integrationMock = mock() val options = SentryOptions() options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) options.addIntegration(integrationMock) -// val expected = HubAdapter.getInstance() - val hub = Hub(options) -// verify(integrationMock).register(expected, options) - hub.clone() + val scopes = createScopes(options) + reset(integrationMock) + scopes.forkedScopes("test") verifyNoMoreInteractions(integrationMock) } @Test - fun `when hub is cloned, scope changes are isolated`() { + fun `when current scope is forked, integrations are not registered`() { + val integrationMock = mock() + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + options.addIntegration(integrationMock) + val scopes = createScopes(options) + reset(integrationMock) + scopes.forkedCurrentScope("test") + verifyNoMoreInteractions(integrationMock) + } + + @Test + fun `when isolation scope is forked, scope changes are isolated`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val scopes = createScopes(options) + var firstScope: IScope? = null + scopes.configureScope { + firstScope = it + it.setTag("scopes", "a") + } + var cloneScope: IScope? = null + val clone = scopes.forkedScopes("test") + clone.configureScope { + cloneScope = it + it.setTag("scopes", "b") + } + assertEquals("a", firstScope!!.tags["scopes"]) + assertEquals("b", cloneScope!!.tags["scopes"]) + } + + @Test + fun `when current scope is forked, scope changes are not isolated`() { val options = SentryOptions() options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val hub = Hub(options) + val scopes = createScopes(options) var firstScope: IScope? = null - hub.configureScope { + scopes.configureScope { firstScope = it - it.setTag("hub", "a") + it.setTag("scopes", "a") } var cloneScope: IScope? = null - val clone = hub.clone() + val clone = scopes.forkedCurrentScope("test") clone.configureScope { cloneScope = it - it.setTag("hub", "b") + it.setTag("scopes", "b") } - assertEquals("a", firstScope!!.tags["hub"]) - assertEquals("b", cloneScope!!.tags["hub"]) + assertEquals("b", firstScope!!.tags["scopes"]) + assertEquals("b", cloneScope!!.tags["scopes"]) } @Test - fun `when hub is initialized, breadcrumbs are capped as per options`() { + fun `when scopes is initialized, breadcrumbs are capped as per options`() { val options = SentryOptions() options.cacheDirPath = file.absolutePath options.maxBreadcrumbs = 5 options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) (1..10).forEach { _ -> sut.addBreadcrumb(Breadcrumb(), null) } var actual = 0 sut.configureScope { @@ -131,7 +185,7 @@ class HubTest { options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { _: Breadcrumb, _: Any? -> null } options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) sut.addBreadcrumb(Breadcrumb(), null) var breadcrumbs: Queue? = null sut.configureScope { breadcrumbs = it.breadcrumbs } @@ -146,7 +200,7 @@ class HubTest { options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb: Breadcrumb, _: Any? -> breadcrumb.message = expected; breadcrumb; } options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) val crumb = Breadcrumb() crumb.message = "original" sut.addBreadcrumb(crumb) @@ -162,7 +216,7 @@ class HubTest { options.beforeBreadcrumb = null options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) val expected = Breadcrumb() sut.addBreadcrumb(expected) var breadcrumbs: Queue? = null @@ -179,7 +233,7 @@ class HubTest { options.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { _: Breadcrumb, _: Any? -> throw exception } options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) val actual = Breadcrumb() sut.addBreadcrumb(actual) @@ -193,7 +247,7 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) assertEquals(SentryId.EMPTY_ID, sut.lastEventId) } @@ -203,9 +257,9 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) var breadcrumbs: Queue? = null - sut.configureScope { breadcrumbs = it.breadcrumbs } + sut.configureScope(ScopeType.COMBINED) { breadcrumbs = it.breadcrumbs } sut.close() sut.addBreadcrumb(Breadcrumb()) assertTrue(breadcrumbs!!.isEmpty()) @@ -217,7 +271,7 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) var breadcrumbs: Queue? = null sut.configureScope { breadcrumbs = it.breadcrumbs } sut.addBreadcrumb("message", "category") @@ -231,7 +285,7 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) var breadcrumbs: Queue? = null sut.configureScope { breadcrumbs = it.breadcrumbs } sut.addBreadcrumb("message", "category") @@ -241,7 +295,7 @@ class HubTest { @Test fun `when flush is called on disabled client, no-op`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.flush(1000) @@ -250,7 +304,7 @@ class HubTest { @Test fun `when flush is called, client flush gets called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.flush(1000) verify(mockClient).flush(1000) @@ -263,14 +317,14 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) sut.callMethod("captureEvent", SentryEvent::class.java, null) assertEquals(SentryId.EMPTY_ID, sut.lastEventId) } @Test fun `when captureEvent is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.captureEvent(SentryEvent()) @@ -279,7 +333,7 @@ class HubTest { @Test fun `when captureEvent is called with a valid argument, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val event = SentryEvent() val hints = HintUtils.createWithTypeCheckHint({}) @@ -288,8 +342,8 @@ class HubTest { } @Test - fun `when captureEvent is called on disabled hub, lastEventId does not get overwritten`() { - val (sut, mockClient) = getEnabledHub() + fun `when captureEvent is called on disabled scopes, lastEventId does not get overwritten`() { + val (sut, mockClient) = getEnabledScopes() whenever(mockClient.captureEvent(any(), any(), anyOrNull())).thenReturn(SentryId(UUID.randomUUID())) val event = SentryEvent() val hints = HintUtils.createWithTypeCheckHint({}) @@ -302,7 +356,7 @@ class HubTest { @Test fun `when captureEvent is called and session tracking is disabled, it should not capture a session`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val event = SentryEvent() val hints = HintUtils.createWithTypeCheckHint({}) @@ -313,7 +367,7 @@ class HubTest { @Test fun `when captureEvent is called but no session started, it should not capture a session`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val event = SentryEvent() val hints = HintUtils.createWithTypeCheckHint({}) @@ -324,7 +378,7 @@ class HubTest { @Test fun `when captureEvent is called and event has exception which has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val exception = RuntimeException() val span = mock() whenever(span.spanContext).thenReturn(SpanContext("op")) @@ -340,7 +394,7 @@ class HubTest { @Test fun `when captureEvent is called and event has exception which root cause has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val rootCause = RuntimeException() val span = mock() whenever(span.spanContext).thenReturn(SpanContext("op")) @@ -356,7 +410,7 @@ class HubTest { @Test fun `when captureEvent is called and event has exception which non-root cause has been previously attached with span context, sets span context to the event`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val rootCause = RuntimeException() val exceptionAssignedToSpan = RuntimeException(rootCause) val span = mock() @@ -373,7 +427,7 @@ class HubTest { @Test fun `when captureEvent is called and event has exception which has been previously attached with span context and trace context already set, does not set new span context to the event`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val exception = RuntimeException() val span = mock() whenever(span.spanContext).thenReturn(SpanContext("op")) @@ -381,7 +435,7 @@ class HubTest { val event = SentryEvent(exception) val originalSpanContext = SpanContext("op") - event.contexts.trace = originalSpanContext + event.contexts.setTrace(originalSpanContext) val hints = HintUtils.createWithTypeCheckHint({}) sut.captureEvent(event, hints) @@ -391,7 +445,7 @@ class HubTest { @Test fun `when captureEvent is called and event has exception which has not been previously attached with span context, does not set new span context to the event`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val event = SentryEvent(RuntimeException()) @@ -403,7 +457,7 @@ class HubTest { @Test fun `when captureEvent is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureEvent(SentryEvent(), null) { it.setTag("test", "testValue") @@ -420,7 +474,7 @@ class HubTest { @Test fun `when captureEvent is called with a ScopeCallback then subsequent calls to captureEvent send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val argumentCaptor = argumentCaptor() sut.captureEvent(SentryEvent(), null) { @@ -441,7 +495,7 @@ class HubTest { @Test fun `when captureEvent is called with a ScopeCallback that crashes then the event should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() + val (sut, mockClient, logger) = getEnabledScopes() val exception = Exception("scope callback exception") sut.captureEvent(SentryEvent(), null) { @@ -465,14 +519,14 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) sut.callMethod("captureMessage", String::class.java, null) assertEquals(SentryId.EMPTY_ID, sut.lastEventId) } @Test fun `when captureMessage is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.captureMessage("test") @@ -481,7 +535,7 @@ class HubTest { @Test fun `when captureMessage is called with a valid message, captureMessage on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureMessage("test") verify(mockClient).captureMessage(any(), any(), any()) @@ -489,14 +543,14 @@ class HubTest { @Test fun `when captureMessage is called, level is INFO by default`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureMessage("test") verify(mockClient).captureMessage(eq("test"), eq(SentryLevel.INFO), any()) } @Test fun `when captureMessage is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureMessage("test") { it.setTag("test", "testValue") @@ -513,7 +567,7 @@ class HubTest { @Test fun `when captureMessage is called with a ScopeCallback then subsequent calls to captureMessage send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val argumentCaptor = argumentCaptor() sut.captureMessage("testMessage") { @@ -534,7 +588,7 @@ class HubTest { @Test fun `when captureMessage is called with a ScopeCallback that crashes then the message should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() + val (sut, mockClient, logger) = getEnabledScopes() val exception = Exception("scope callback exception") sut.captureMessage("Hello World") { @@ -559,14 +613,14 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) sut.callMethod("captureException", Throwable::class.java, null) assertEquals(SentryId.EMPTY_ID, sut.lastEventId) } @Test fun `when captureException is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.captureException(Throwable()) @@ -575,7 +629,7 @@ class HubTest { @Test fun `when captureException is called with a valid argument and hint, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val hints = HintUtils.createWithTypeCheckHint({}) sut.captureException(Throwable(), hints) @@ -584,7 +638,7 @@ class HubTest { @Test fun `when captureException is called with a valid argument but no hint, captureEvent on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureException(Throwable()) verify(mockClient).captureEvent(any(), any(), any()) @@ -592,7 +646,7 @@ class HubTest { @Test fun `when captureException is called with an exception which has been previously attached with span context, span context should be set on the event before capturing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val throwable = Throwable() val span = mock() whenever(span.spanContext).thenReturn(SpanContext("op")) @@ -611,7 +665,7 @@ class HubTest { @Test fun `when captureException is called with an exception which has not been previously attached with span context, span context should not be set on the event before capturing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val span = mock() whenever(span.spanContext).thenReturn(SpanContext("op")) sut.setSpanContext(Throwable(), span, "tx-name") @@ -628,7 +682,7 @@ class HubTest { @Test fun `when captureException is called with a ScopeCallback then the modified scope is sent to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureException(Throwable(), null) { it.setTag("test", "testValue") @@ -645,7 +699,7 @@ class HubTest { @Test fun `when captureException is called with a ScopeCallback then subsequent calls to captureException send the unmodified Scope to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() val argumentCaptor = argumentCaptor() sut.captureException(Throwable(), null) { @@ -666,7 +720,7 @@ class HubTest { @Test fun `when captureException is called with a ScopeCallback that crashes then the exception should still be captured`() { - val (sut, mockClient, logger) = getEnabledHub() + val (sut, mockClient, logger) = getEnabledScopes() val exception = Exception("scope callback exception") sut.captureException(Throwable()) { @@ -688,7 +742,7 @@ class HubTest { @Test fun `when captureUserFeedback is called it is forwarded to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureUserFeedback(userFeedback) verify(mockClient).captureUserFeedback( @@ -703,7 +757,7 @@ class HubTest { @Test fun `when captureUserFeedback is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.captureUserFeedback(userFeedback) @@ -712,7 +766,7 @@ class HubTest { @Test fun `when captureUserFeedback is called and client throws, don't crash`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() whenever(mockClient.captureUserFeedback(any())).doThrow(IllegalArgumentException("")) @@ -732,7 +786,7 @@ class HubTest { @Test fun `when captureCheckIn is called it is forwarded to the client`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.captureCheckIn(checkIn) verify(mockClient).captureCheckIn( @@ -748,7 +802,7 @@ class HubTest { @Test fun `when captureCheckIn is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.captureCheckIn(checkIn) @@ -757,7 +811,7 @@ class HubTest { @Test fun `when captureCheckIn is called and client throws, don't crash`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() whenever(mockClient.captureCheckIn(any(), any(), anyOrNull())).doThrow(IllegalArgumentException("")) @@ -771,7 +825,7 @@ class HubTest { //region close tests @Test fun `when close is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() sut.close() @@ -780,7 +834,7 @@ class HubTest { @Test fun `when close is called and client is alive, close on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close() verify(mockClient).close(eq(false)) @@ -788,7 +842,7 @@ class HubTest { @Test fun `when close is called with isRestarting false and client is alive, close on the client should be called with isRestarting false`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close(false) verify(mockClient).close(eq(false)) @@ -796,7 +850,7 @@ class HubTest { @Test fun `when close is called with isRestarting true and client is alive, close on the client should be called with isRestarting true`() { - val (sut, mockClient) = getEnabledHub() + val (sut, mockClient) = getEnabledScopes() sut.close(true) verify(mockClient).close(eq(true)) @@ -806,7 +860,7 @@ class HubTest { //region withScope tests @Test fun `when withScope is called on disabled client, execute on NoOpScope`() { - val (sut) = getEnabledHub() + val (sut) = getEnabledScopes() val scopeCallback = mock() sut.close() @@ -817,7 +871,7 @@ class HubTest { @Test fun `when withScope is called with alive client, run should be called`() { - val (sut) = getEnabledHub() + val (sut) = getEnabledScopes() val scopeCallback = mock() @@ -827,14 +881,51 @@ class HubTest { @Test fun `when withScope throws an exception then it should be caught`() { - val (hub, _, logger) = getEnabledHub() + val (scopes, _, logger) = getEnabledScopes() val exception = Exception("scope callback exception") val scopeCallback = ScopeCallback { throw exception } - hub.withScope(scopeCallback) + scopes.withScope(scopeCallback) + + verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) + } + //endregion + + //region withIsolationScope tests + @Test + fun `when withIsolationScope is called on disabled client, execute on NoOpScope`() { + val (sut) = getEnabledScopes() + + val scopeCallback = mock() + sut.close() + + sut.withIsolationScope(scopeCallback) + verify(scopeCallback).run(NoOpScope.getInstance()) + } + + @Test + fun `when withIsolationScope is called with alive client, run should be called`() { + val (sut) = getEnabledScopes() + + val scopeCallback = mock() + + sut.withIsolationScope(scopeCallback) + verify(scopeCallback).run(any()) + } + + @Test + fun `when withIsolationScope throws an exception then it should be caught`() { + val (scopes, _, logger) = getEnabledScopes() + + val exception = Exception("scope callback exception") + val scopeCallback = ScopeCallback { + throw exception + } + + scopes.withIsolationScope(scopeCallback) verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) } @@ -843,7 +934,7 @@ class HubTest { //region configureScope tests @Test fun `when configureScope is called on disabled client, do nothing`() { - val (sut) = getEnabledHub() + val (sut) = getEnabledScopes() val scopeCallback = mock() sut.close() @@ -854,7 +945,7 @@ class HubTest { @Test fun `when configureScope is called with alive client, run should be called`() { - val (sut) = getEnabledHub() + val (sut) = getEnabledScopes() val scopeCallback = mock() @@ -864,26 +955,26 @@ class HubTest { @Test fun `when configureScope throws an exception then it should be caught`() { - val (hub, _, logger) = getEnabledHub() + val (scopes, _, logger) = getEnabledScopes() val exception = Exception("scope callback exception") val scopeCallback = ScopeCallback { throw exception } - hub.configureScope(scopeCallback) + scopes.configureScope(scopeCallback) verify(logger).log(eq(SentryLevel.ERROR), any(), eq(exception)) } //endregion @Test - fun `when integration is registered, hub is enabled`() { + fun `when integration is registered, scopes is enabled`() { val mock = mock() var options: SentryOptions? = null - // init main hub and make it enabled - Sentry.init { + // init main scopes and make it enabled + initForTest { it.addIntegration(mock) it.dsn = "https://key@sentry.io/proj" it.cacheDirPath = file.absolutePath @@ -892,8 +983,8 @@ class HubTest { } doAnswer { - val hub = it.arguments[0] as IHub - assertTrue(hub.isEnabled) + val scopes = it.arguments[0] as IScopes + assertTrue(scopes.isEnabled) }.whenever(mock).register(any(), eq(options!!)) verify(mock).register(any(), eq(options!!)) @@ -902,26 +993,26 @@ class HubTest { //region setLevel tests @Test fun `when setLevel is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() - hub.setLevel(SentryLevel.INFO) + scopes.setLevel(SentryLevel.INFO) assertNull(scope?.level) } @Test fun `when setLevel is called, level is set`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.setLevel(SentryLevel.INFO) + scopes.setLevel(SentryLevel.INFO) assertEquals(SentryLevel.INFO, scope?.level) } //endregion @@ -929,74 +1020,74 @@ class HubTest { //region setTransaction tests @Test fun `when setTransaction is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() - hub.setTransaction("test") + scopes.setTransaction("test") assertNull(scope?.transactionName) } @Test fun `when setTransaction is called, and transaction is not set, transaction name is changed`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.setTransaction("test") + scopes.setTransaction("test") assertEquals("test", scope?.transactionName) } @Test fun `when setTransaction is called, and transaction is set, transaction name is changed`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - val tx = hub.startTransaction("test", "op") - hub.configureScope { it.setTransaction(tx) } + val tx = scopes.startTransaction("test", "op") + scopes.configureScope { it.setTransaction(tx) } assertEquals("test", scope?.transactionName) } @Test fun `when startTransaction is called with different instrumenter, no-op is returned`() { - val hub = generateHub() + val scopes = generateScopes() val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } val transactionOptions = TransactionOptions() - val tx = hub.startTransaction(transactionContext, transactionOptions) + val tx = scopes.startTransaction(transactionContext, transactionOptions) assertTrue(tx is NoOpTransaction) } @Test fun `when startTransaction is called with different instrumenter, no-op is returned 2`() { - val hub = generateHub() { + val scopes = generateScopes() { it.instrumenter = Instrumenter.OTEL } - val tx = hub.startTransaction("test", "op") + val tx = scopes.startTransaction("test", "op") assertTrue(tx is NoOpTransaction) } @Test fun `when startTransaction is called with configured instrumenter, it works`() { - val hub = generateHub() { + val scopes = generateScopes() { it.instrumenter = Instrumenter.OTEL } val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } val transactionOptions = TransactionOptions() - val tx = hub.startTransaction(transactionContext, transactionOptions) + val tx = scopes.startTransaction(transactionContext, transactionOptions) assertFalse(tx is NoOpTransaction) } @@ -1005,27 +1096,27 @@ class HubTest { //region setUser tests @Test fun `when setUser is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() - hub.setUser(User()) + scopes.setUser(User()) assertNull(scope?.user) } @Test fun `when setUser is called, user is set`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } val user = User() - hub.setUser(user) + scopes.setUser(user) assertEquals(user, scope?.user) } //endregion @@ -1033,40 +1124,40 @@ class HubTest { //region setFingerprint tests @Test fun `when setFingerprint is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() val fingerprint = listOf("abc") - hub.setFingerprint(fingerprint) + scopes.setFingerprint(fingerprint) assertEquals(0, scope?.fingerprint?.count()) } @Test fun `when setFingerprint is called with null parameter, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.callMethod("setFingerprint", List::class.java, null) + scopes.callMethod("setFingerprint", List::class.java, null) assertEquals(0, scope?.fingerprint?.count()) } @Test fun `when setFingerprint is called, fingerprint is set`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } val fingerprint = listOf("abc") - hub.setFingerprint(fingerprint) + scopes.setFingerprint(fingerprint) assertEquals(1, scope?.fingerprint?.count()) } //endregion @@ -1074,30 +1165,30 @@ class HubTest { //region clearBreadcrumbs tests @Test fun `when clearBreadcrumbs is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.addBreadcrumb(Breadcrumb()) + scopes.addBreadcrumb(Breadcrumb()) assertEquals(1, scope?.breadcrumbs?.count()) - hub.close() + scopes.close() assertEquals(0, scope?.breadcrumbs?.count()) } @Test fun `when clearBreadcrumbs is called, clear breadcrumbs`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.addBreadcrumb(Breadcrumb()) + scopes.addBreadcrumb(Breadcrumb()) assertEquals(1, scope?.breadcrumbs?.count()) - hub.clearBreadcrumbs() + scopes.clearBreadcrumbs() assertEquals(0, scope?.breadcrumbs?.count()) } //endregion @@ -1105,38 +1196,38 @@ class HubTest { //region setTag tests @Test fun `when setTag is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() - hub.setTag("test", "test") + scopes.setTag("test", "test") assertEquals(0, scope?.tags?.count()) } @Test fun `when setTag is called with null parameters, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.callMethod("setTag", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) + scopes.callMethod("setTag", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) assertEquals(0, scope?.tags?.count()) } @Test fun `when setTag is called, tag is set`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.setTag("test", "test") + scopes.setTag("test", "test") assertEquals(1, scope?.tags?.count()) } //endregion @@ -1144,38 +1235,38 @@ class HubTest { //region setExtra tests @Test fun `when setExtra is called on disabled client, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.close() + scopes.close() - hub.setExtra("test", "test") + scopes.setExtra("test", "test") assertEquals(0, scope?.extras?.count()) } @Test fun `when setExtra is called with null parameters, do nothing`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.callMethod("setExtra", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) + scopes.callMethod("setExtra", parameterTypes = arrayOf(String::class.java, String::class.java), null, null) assertEquals(0, scope?.extras?.count()) } @Test fun `when setExtra is called, extra is set`() { - val hub = generateHub() + val scopes = generateScopes() var scope: IScope? = null - hub.configureScope { + scopes.configureScope { scope = it } - hub.setExtra("test", "test") + scopes.setExtra("test", "test") assertEquals(1, scope?.extras?.count()) } //endregion @@ -1187,7 +1278,7 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) + val sut = createScopes(options) try { sut.callMethod("captureEnvelope", SentryEnvelope::class.java, null) fail() @@ -1202,8 +1293,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock(enabled = false) sut.bindClient(mockClient) sut.close() @@ -1217,8 +1308,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val envelope = SentryEnvelope(SentryId(UUID.randomUUID()), null, setOf()) @@ -1232,8 +1323,8 @@ class HubTest { dsn = "https://key@sentry.io/proj" setSerializer(mock()) } - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) whenever(mockClient.captureEnvelope(any(), anyOrNull())).thenReturn(SentryId()) val envelope = SentryEnvelope(SentryId(UUID.randomUUID()), null, setOf()) @@ -1250,11 +1341,15 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.close() + sut.configureScope(ScopeType.ISOLATION) { scope -> + scope.client.isEnabled + } + sut.startSession() verify(mockClient, never()).captureSession(any(), any()) } @@ -1266,8 +1361,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.startSession() @@ -1281,8 +1376,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.startSession() @@ -1300,8 +1395,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock(enabled = false) sut.bindClient(mockClient) sut.close() @@ -1316,8 +1411,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.endSession() @@ -1331,8 +1426,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.startSession() @@ -1348,8 +1443,8 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.release = "0.0.1" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.endSession() @@ -1364,8 +1459,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) sut.close() @@ -1382,8 +1477,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), sut) @@ -1398,8 +1493,8 @@ class HubTest { dsn = "https://key@sentry.io/proj" setSerializer(mock()) } - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) whenever(mockClient.captureTransaction(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(SentryId()) @@ -1414,8 +1509,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), sut) @@ -1429,8 +1524,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) @@ -1445,8 +1540,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) @@ -1469,8 +1564,8 @@ class HubTest { options.cacheDirPath = file.absolutePath options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) val mockBackpressureMonitor = mock() options.backpressureMonitor = mockBackpressureMonitor @@ -1496,21 +1591,21 @@ class HubTest { @Test fun `when startTransaction and profiling is enabled, transaction is profiled only if sampled`() { val mockTransactionProfiler = mock() - val mockClient = mock() + val mockClient = createSentryClientMock() whenever(mockTransactionProfiler.onTransactionFinish(any(), anyOrNull(), anyOrNull())).thenAnswer { mockClient.captureEnvelope(mock()) } - val hub = generateHub { + val scopes = generateScopes { it.setTransactionProfiler(mockTransactionProfiler) } - hub.bindClient(mockClient) + scopes.bindClient(mockClient) // Transaction is not sampled, so it should not be profiled val contexts = TransactionContext("name", "op", TracesSamplingDecision(false, null, true, null)) - val transaction = hub.startTransaction(contexts) + val transaction = scopes.startTransaction(contexts) transaction.finish() verify(mockClient, never()).captureEnvelope(any()) // Transaction is sampled, so it should be profiled val sampledContexts = TransactionContext("name", "op", TracesSamplingDecision(true, null, true, null)) - val sampledTransaction = hub.startTransaction(sampledContexts) + val sampledTransaction = scopes.startTransaction(sampledContexts) sampledTransaction.finish() verify(mockClient).captureEnvelope(any()) } @@ -1518,15 +1613,15 @@ class HubTest { @Test fun `when startTransaction and is sampled but profiling is disabled, transaction is not profiled`() { val mockTransactionProfiler = mock() - val mockClient = mock() + val mockClient = createSentryClientMock() whenever(mockTransactionProfiler.onTransactionFinish(any(), anyOrNull(), anyOrNull())).thenAnswer { mockClient.captureEnvelope(mock()) } - val hub = generateHub { + val scopes = generateScopes { it.profilesSampleRate = 0.0 it.setTransactionProfiler(mockTransactionProfiler) } - hub.bindClient(mockClient) + scopes.bindClient(mockClient) val contexts = TransactionContext("name", "op") - val transaction = hub.startTransaction(contexts) + val transaction = scopes.startTransaction(contexts) transaction.finish() verify(mockClient, never()).captureEnvelope(any()) } @@ -1535,12 +1630,12 @@ class HubTest { fun `when profiler is running and isAppStartTransaction is false, startTransaction does not interact with profiler`() { val mockTransactionProfiler = mock() whenever(mockTransactionProfiler.isRunning).thenReturn(true) - val hub = generateHub { + val scopes = generateScopes { it.profilesSampleRate = 1.0 it.setTransactionProfiler(mockTransactionProfiler) } val context = TransactionContext("name", "op") - hub.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) + scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) verify(mockTransactionProfiler, never()).start() verify(mockTransactionProfiler, never()).bindTransaction(any()) } @@ -1549,12 +1644,12 @@ class HubTest { fun `when profiler is running and isAppStartTransaction is true, startTransaction binds current profile`() { val mockTransactionProfiler = mock() whenever(mockTransactionProfiler.isRunning).thenReturn(true) - val hub = generateHub { + val scopes = generateScopes { it.profilesSampleRate = 1.0 it.setTransactionProfiler(mockTransactionProfiler) } val context = TransactionContext("name", "op") - val transaction = hub.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = true }) + val transaction = scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = true }) verify(mockTransactionProfiler, never()).start() verify(mockTransactionProfiler).bindTransaction(eq(transaction)) } @@ -1563,12 +1658,12 @@ class HubTest { fun `when profiler is not running, startTransaction starts and binds current profile`() { val mockTransactionProfiler = mock() whenever(mockTransactionProfiler.isRunning).thenReturn(false) - val hub = generateHub { + val scopes = generateScopes { it.profilesSampleRate = 1.0 it.setTransactionProfiler(mockTransactionProfiler) } val context = TransactionContext("name", "op") - val transaction = hub.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) + val transaction = scopes.startTransaction(context, TransactionOptions().apply { isAppStartTransaction = false }) verify(mockTransactionProfiler).start() verify(mockTransactionProfiler).bindTransaction(eq(transaction)) } @@ -1577,75 +1672,75 @@ class HubTest { //region startTransaction tests @Test fun `when startTransaction, creates transaction`() { - val hub = generateHub() + val scopes = generateScopes() val contexts = TransactionContext("name", "op") - val transaction = hub.startTransaction(contexts) + val transaction = scopes.startTransaction(contexts) assertTrue(transaction is SentryTracer) assertEquals(contexts, transaction.root.spanContext) } @Test fun `when startTransaction with bindToScope set to false, transaction is not attached to the scope`() { - val hub = generateHub() + val scopes = generateScopes() - hub.startTransaction("name", "op", TransactionOptions()) + scopes.startTransaction("name", "op", TransactionOptions()) - hub.configureScope { + scopes.configureScope { assertNull(it.span) } } @Test fun `when startTransaction without bindToScope set, transaction is not attached to the scope`() { - val hub = generateHub() + val scopes = generateScopes() - hub.startTransaction("name", "op") + scopes.startTransaction("name", "op") - hub.configureScope { + scopes.configureScope { assertNull(it.span) } } @Test fun `when startTransaction with bindToScope set to true, transaction is attached to the scope`() { - val hub = generateHub() + val scopes = generateScopes() - val transaction = hub.startTransaction("name", "op", TransactionOptions().also { it.isBindToScope = true }) + val transaction = scopes.startTransaction("name", "op", TransactionOptions().also { it.isBindToScope = true }) - hub.configureScope { + scopes.configureScope { assertEquals(transaction, it.span) } } @Test fun `when startTransaction and no tracing sampling is configured, event is not sampled`() { - val hub = generateHub { + val scopes = generateScopes { it.tracesSampleRate = 0.0 } - val transaction = hub.startTransaction("name", "op") + val transaction = scopes.startTransaction("name", "op") assertFalse(transaction.isSampled!!) } @Test fun `when startTransaction and no profile sampling is configured, profile is not sampled`() { - val hub = generateHub { + val scopes = generateScopes { it.tracesSampleRate = 1.0 it.profilesSampleRate = 0.0 } - val transaction = hub.startTransaction("name", "op") + val transaction = scopes.startTransaction("name", "op") assertTrue(transaction.isSampled!!) assertFalse(transaction.isProfileSampled!!) } @Test fun `when startTransaction with parent sampled and no traces sampler provided, transaction inherits sampling decision`() { - val hub = generateHub() + val scopes = generateScopes() val transactionContext = TransactionContext("name", "op") transactionContext.parentSampled = true - val transaction = hub.startTransaction(transactionContext) + val transaction = scopes.startTransaction(transactionContext) assertNotNull(transaction) assertNotNull(transaction.isSampled) assertTrue(transaction.isSampled!!) @@ -1653,40 +1748,43 @@ class HubTest { @Test fun `when startTransaction with parent profile sampled and no profile sampler provided, transaction inherits profile sampling decision`() { - val hub = generateHub() + val scopes = generateScopes() val transactionContext = TransactionContext("name", "op") transactionContext.setParentSampled(true, true) - val transaction = hub.startTransaction(transactionContext) + val transaction = scopes.startTransaction(transactionContext) assertTrue(transaction.isProfileSampled!!) } @Test - fun `Hub should close the sentry executor processor, profiler and performance collector on close call`() { + fun `Scopes should close the sentry executor processor, profiler and performance collector on close call`() { val executor = mock() val profiler = mock() val performanceCollector = mock() + val backpressureMonitorMock = mock() val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" cacheDirPath = file.absolutePath executorService = executor setTransactionProfiler(profiler) transactionPerformanceCollector = performanceCollector + backpressureMonitor = backpressureMonitorMock } - val sut = Hub(options) + val sut = createScopes(options) sut.close() + verify(backpressureMonitorMock).close() verify(executor).close(any()) verify(profiler).close() verify(performanceCollector).close() } @Test - fun `Hub with isRestarting true should close the sentry executor in the background`() { + fun `Scopes with isRestarting true should close the sentry executor in the background`() { val executor = spy(DeferredExecutorService()) val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" executorService = executor } - val sut = Hub(options) + val sut = createScopes(options) sut.close(true) verify(executor, never()).close(any()) executor.runAll() @@ -1694,31 +1792,32 @@ class HubTest { } @Test - fun `Hub with isRestarting false should close the sentry executor in the background`() { + fun `Scopes with isRestarting false should close the sentry executor in the background`() { val executor = mock() val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" executorService = executor } - val sut = Hub(options) + val sut = createScopes(options) sut.close(false) verify(executor).close(any()) } @Test - fun `Hub close should clear the scope`() { + fun `Scopes close should clear the scope`() { val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - val sut = Hub(options) + val sut = createScopes(options) sut.addBreadcrumb("Test") sut.startTransaction("test", "test.op", TransactionOptions().also { it.isBindToScope = true }) sut.close() // we have to clone the scope, so its isEnabled returns true, but it's still built up from // the old scope preserving its data - val clone = sut.clone() + val clone = sut.forkedScopes("test") + clone.bindClient(createSentryClientMock(enabled = true)) var oldScope: IScope? = null clone.configureScope { scope -> oldScope = scope } assertNull(oldScope!!.transaction) @@ -1727,93 +1826,70 @@ class HubTest { @Test fun `when tracesSampleRate and tracesSampler are not set on SentryOptions, startTransaction returns NoOp`() { - val hub = generateHub { + val scopes = generateScopes { it.tracesSampleRate = null it.tracesSampler = null } - val transaction = hub.startTransaction(TransactionContext("name", "op", TracesSamplingDecision(true))) + val transaction = scopes.startTransaction(TransactionContext("name", "op", TracesSamplingDecision(true))) assertTrue(transaction is NoOpTransaction) } //endregion - //region startTransaction tests - @Test - fun `when traceHeaders and no transaction is active, traceHeaders are generated from scope`() { - val hub = generateHub() - - var spanId: SpanId? = null - hub.configureScope { spanId = it.propagationContext.spanId } - - val traceHeader = hub.traceHeaders() - assertNotNull(traceHeader) - assertEquals(spanId, traceHeader.spanId) - } - - @Test - fun `when traceHeaders and there is an active transaction, traceHeaders are not null`() { - val hub = generateHub() - val tx = hub.startTransaction("aTransaction", "op") - hub.configureScope { it.setTransaction(tx) } - - assertNotNull(hub.traceHeaders()) - } - //endregion - //region getSpan tests @Test fun `when there is no active transaction, getSpan returns null`() { - val hub = generateHub() - assertNull(hub.span) + val scopes = generateScopes() + assertNull(scopes.span) } @Test fun `when there is no active transaction, getTransaction returns null`() { - val hub = generateHub() - assertNull(hub.transaction) + val scopes = generateScopes() + assertNull(scopes.transaction) } @Test fun `when there is active transaction bound to the scope, getTransaction and getSpan return active transaction`() { - val hub = generateHub() - val tx = hub.startTransaction("aTransaction", "op") - hub.configureScope { it.transaction = tx } + val scopes = generateScopes() + val tx = scopes.startTransaction("aTransaction", "op") + scopes.configureScope { it.transaction = tx } - assertEquals(tx, hub.transaction) - assertEquals(tx, hub.span) + assertEquals(tx, scopes.transaction) + assertEquals(tx, scopes.span) } @Test - fun `when there is a transaction but the hub is closed, getTransaction returns null`() { - val hub = generateHub() - hub.startTransaction("name", "op") - hub.close() + fun `when there is a transaction but the scopes is closed, getTransaction returns null`() { + val scopes = generateScopes() + scopes.startTransaction("name", "op") + scopes.close() - assertNull(hub.transaction) + assertNull(scopes.transaction) } @Test fun `when there is active span within a transaction bound to the scope, getSpan returns active span`() { - val hub = generateHub() - val tx = hub.startTransaction("aTransaction", "op") - hub.configureScope { it.setTransaction(tx) } - hub.configureScope { it.setTransaction(tx) } + val scopes = generateScopes() + val tx = scopes.startTransaction("aTransaction", "op") + scopes.configureScope { it.setTransaction(tx) } + scopes.configureScope { it.setTransaction(tx) } val span = tx.startChild("op") - assertEquals(tx, hub.transaction) - assertEquals(span, hub.span) + assertEquals(tx, scopes.transaction) + assertEquals(span, scopes.span) } // endregion //region setSpanContext @Test fun `associates span context with throwable`() { - val (hub, mockClient) = getEnabledHub() - val transaction = hub.startTransaction("aTransaction", "op") + val (scopes, mockClient) = getEnabledScopes() + val transaction = scopes.startTransaction("aTransaction", "op") val span = transaction.startChild("op") val exception = RuntimeException() - hub.setSpanContext(exception, span, "tx-name") - hub.captureEvent(SentryEvent(exception)) + scopes.setSpanContext(exception, span, "tx-name") + scopes.captureEvent(SentryEvent(exception)) verify(mockClient).captureEvent( check { @@ -1823,12 +1899,6 @@ class HubTest { anyOrNull() ) } - - @Test - fun `returns null when no span context associated with throwable`() { - val hub = generateHub() as Hub - assertNull(hub.getSpanContext(RuntimeException())) - } // endregion @Test @@ -1836,9 +1906,9 @@ class HubTest { val nativeMarker = File(hashedFolder(), EnvelopeCache.NATIVE_CRASH_MARKER_FILE) nativeMarker.mkdirs() nativeMarker.createNewFile() - val hub = generateHub() as Hub + val scopes = generateScopes() as Scopes - assertTrue(hub.isCrashedLastRun!!) + assertTrue(scopes.isCrashedLastRun!!) assertTrue(nativeMarker.exists()) } @@ -1847,89 +1917,99 @@ class HubTest { val nativeMarker = File(hashedFolder(), EnvelopeCache.NATIVE_CRASH_MARKER_FILE) nativeMarker.mkdirs() nativeMarker.createNewFile() - val hub = generateHub { + val scopes = generateScopes { it.isEnableAutoSessionTracking = false } - assertTrue(hub.isCrashedLastRun!!) + assertTrue(scopes.isCrashedLastRun!!) assertFalse(nativeMarker.exists()) } @Test fun `reportFullyDisplayed is ignored if TimeToFullDisplayTracing is disabled`() { var called = false - val hub = generateHub { + val scopes = generateScopes { it.fullyDisplayedReporter.registerFullyDrawnListener { called = !called } } - hub.reportFullyDisplayed() + scopes.reportFullyDisplayed() assertFalse(called) } @Test fun `reportFullyDisplayed calls FullyDisplayedReporter if TimeToFullDisplayTracing is enabled`() { var called = false - val hub = generateHub { + val scopes = generateScopes { it.isEnableTimeToFullDisplayTracing = true it.fullyDisplayedReporter.registerFullyDrawnListener { called = !called } } - hub.reportFullyDisplayed() + scopes.reportFullyDisplayed() assertTrue(called) } @Test fun `reportFullyDisplayed calls FullyDisplayedReporter only once`() { var called = false - val hub = generateHub { + val scopes = generateScopes { it.isEnableTimeToFullDisplayTracing = true it.fullyDisplayedReporter.registerFullyDrawnListener { called = !called } } - hub.reportFullyDisplayed() + scopes.reportFullyDisplayed() assertTrue(called) - hub.reportFullyDisplayed() + scopes.reportFullyDisplayed() assertTrue(called) } @Test - fun `reportFullDisplayed calls reportFullyDisplayed`() { - val hub = spy(generateHub()) - hub.reportFullDisplayed() - verify(hub).reportFullyDisplayed() + fun `continueTrace creates propagation context from headers and returns transaction context if performance enabled`() { + val scopes = generateScopes() + val traceId = SentryId() + val parentSpanId = SpanId() + val transactionContext = scopes.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) + + scopes.configureScope { scope -> + assertEquals(traceId, scope.propagationContext.traceId) + assertEquals(parentSpanId, scope.propagationContext.parentSpanId) + } + + assertEquals(traceId, transactionContext!!.traceId) + assertEquals(parentSpanId, transactionContext!!.parentSpanId) } @Test - fun `continueTrace creates propagation context from headers and returns transaction context if performance enabled`() { - val hub = generateHub() + fun `continueTrace creates propagation context from headers and returns transaction context if performance enabled no sampled value`() { + val scopes = generateScopes() val traceId = SentryId() val parentSpanId = SpanId() - val transactionContext = hub.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) + val transactionContext = scopes.continueTrace("$traceId-$parentSpanId", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - hub.configureScope { scope -> + scopes.configureScope { scope -> assertEquals(traceId, scope.propagationContext.traceId) assertEquals(parentSpanId, scope.propagationContext.parentSpanId) } assertEquals(traceId, transactionContext!!.traceId) assertEquals(parentSpanId, transactionContext!!.parentSpanId) + assertEquals(null, transactionContext!!.parentSamplingDecision) } @Test fun `continueTrace creates new propagation context if header invalid and returns transaction context if performance enabled`() { - val hub = generateHub() + val scopes = generateScopes() val traceId = SentryId() var propagationContextHolder = AtomicReference() - hub.configureScope { propagationContextHolder.set(it.propagationContext) } + scopes.configureScope { propagationContextHolder.set(it.propagationContext) } val propagationContextAtStart = propagationContextHolder.get()!! - val transactionContext = hub.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) + val transactionContext = scopes.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - hub.configureScope { scope -> + scopes.configureScope { scope -> assertNotEquals(propagationContextAtStart.traceId, scope.propagationContext.traceId) assertNotEquals(propagationContextAtStart.parentSpanId, scope.propagationContext.parentSpanId) assertNotEquals(propagationContextAtStart.spanId, scope.propagationContext.spanId) @@ -1942,12 +2022,12 @@ class HubTest { @Test fun `continueTrace creates propagation context from headers and returns null if performance disabled`() { - val hub = generateHub { it.enableTracing = false } + val scopes = generateScopes { it.tracesSampleRate = null } val traceId = SentryId() val parentSpanId = SpanId() - val transactionContext = hub.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) + val transactionContext = scopes.continueTrace("$traceId-$parentSpanId-1", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - hub.configureScope { scope -> + scopes.configureScope { scope -> assertEquals(traceId, scope.propagationContext.traceId) assertEquals(parentSpanId, scope.propagationContext.parentSpanId) } @@ -1957,16 +2037,16 @@ class HubTest { @Test fun `continueTrace creates new propagation context if header invalid and returns null if performance disabled`() { - val hub = generateHub { it.enableTracing = false } + val scopes = generateScopes { it.tracesSampleRate = null } val traceId = SentryId() var propagationContextHolder = AtomicReference() - hub.configureScope { propagationContextHolder.set(it.propagationContext) } + scopes.configureScope { propagationContextHolder.set(it.propagationContext) } val propagationContextAtStart = propagationContextHolder.get()!! - val transactionContext = hub.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) + val transactionContext = scopes.continueTrace("invalid", listOf("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET")) - hub.configureScope { scope -> + scopes.configureScope { scope -> assertNotEquals(propagationContextAtStart.traceId, scope.propagationContext.traceId) assertNotEquals(propagationContextAtStart.parentSpanId, scope.propagationContext.parentSpanId) assertNotEquals(propagationContextAtStart.spanId, scope.propagationContext.spanId) @@ -1975,161 +2055,95 @@ class HubTest { assertNull(transactionContext) } + // region replay event tests @Test - fun `hub provides no tags for metrics, if metric option is disabled`() { - val hub = generateHub { - it.isEnableMetrics = false - it.isEnableDefaultTagsForMetrics = true - } as Hub + fun `when captureReplay is called on disabled client, do nothing`() { + val (sut, mockClient) = getEnabledScopes() + sut.close() - assertTrue( - hub.defaultTagsForMetrics.isEmpty() - ) + sut.captureReplay(SentryReplayEvent(), Hint()) + verify(mockClient, never()).captureReplayEvent(any(), any(), any()) } @Test - fun `hub provides no tags for metrics, if default tags option is disabled`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = false - } as Hub + fun `when captureReplay is called with a valid argument, captureReplay on the client should be called`() { + val (sut, mockClient) = getEnabledScopes() - assertTrue( - hub.defaultTagsForMetrics.isEmpty() - ) + val event = SentryReplayEvent() + val hints = HintUtils.createWithTypeCheckHint({}) + sut.captureReplay(event, hints) + verify(mockClient).captureReplayEvent(eq(event), any(), eq(hints)) } + // endregion replay event tests @Test - fun `hub provides minimum default tags for metrics, if nothing is set up`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = true - } as Hub - - assertEquals( - mapOf( - "environment" to "production" - ), - hub.defaultTagsForMetrics - ) + fun `is considered enabled if client is enabled()`() { + val scopes = generateScopes() as Scopes + val client = mock() + whenever(client.isEnabled).thenReturn(true) + scopes.bindClient(client) + assertTrue(scopes.isEnabled) } @Test - fun `hub provides default tags for metrics, based on options and running transaction`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableDefaultTagsForMetrics = true - it.environment = "test" - it.release = "1.0" - } as Hub - hub.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertEquals( - mapOf( - "environment" to "test", - "release" to "1.0", - "transaction" to "name" - ), - hub.defaultTagsForMetrics - ) + fun `is considered disabled if client is disabled()`() { + val scopes = generateScopes() as Scopes + val client = mock() + whenever(client.isEnabled).thenReturn(false) + scopes.bindClient(client) + assertFalse(scopes.isEnabled) } @Test - fun `hub provides no local metric aggregator if metrics feature is disabled`() { - val hub = generateHub { - it.isEnableMetrics = false - it.isEnableSpanLocalMetricAggregation = true - } as Hub - - hub.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertNull(hub.localMetricsAggregator) - } - - @Test - fun `hub provides no local metric aggregator if local aggregation feature is disabled`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = false - } as Hub - - hub.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - - assertNull(hub.localMetricsAggregator) - } + fun `creating a transaction with an ignored origin noops`() { + val scopes = generateScopes { + it.setIgnoredSpanOrigins(listOf("ignored.span.origin")) + } - @Test - fun `hub provides local metric aggregator if feature is enabled`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = true - } as Hub + val transactionContext = TransactionContext("transaction-name", "transaction-op") + val transactionOptions = TransactionOptions().also { + it.origin = "ignored.span.origin" + it.isBindToScope = true + } - hub.startTransaction( - "name", - "op", - TransactionOptions().apply { isBindToScope = true } - ) - assertNotNull(hub.localMetricsAggregator) + val transaction = scopes.startTransaction(transactionContext, transactionOptions) + assertTrue(transaction.isNoOp) + scopes.configureScope { assertNull(it.transaction) } } @Test - fun `hub startSpanForMetric starts a child span`() { - val hub = generateHub { - it.isEnableMetrics = true - it.isEnableSpanLocalMetricAggregation = true - it.sampleRate = 1.0 - } as Hub - - val txn = hub.startTransaction( - "name.txn", - "op.txn", - TransactionOptions().apply { isBindToScope = true } - ) + fun `creating a transaction with a non ignored origin creates the transaction`() { + val scopes = generateScopes { + it.setIgnoredSpanOrigins(listOf("ignored.span.origin")) + } - val span = hub.startSpanForMetric("op", "key")!! + val transactionContext = TransactionContext("transaction-name", "transaction-op") + val transactionOptions = TransactionOptions().also { + it.origin = "other.span.origin" + it.isBindToScope = true + } - assertEquals("op", span.spanContext.op) - assertEquals("key", span.spanContext.description) - assertEquals(span.spanContext.parentSpanId, txn.spanContext.spanId) + val transaction = scopes.startTransaction(transactionContext, transactionOptions) + assertFalse(transaction.isNoOp) + scopes.configureScope { assertSame(transaction, it.transaction) } } - // region replay event tests @Test - fun `when captureReplay is called on disabled client, do nothing`() { - val (sut, mockClient) = getEnabledHub() - sut.close() - - sut.captureReplay(SentryReplayEvent(), Hint()) - verify(mockClient, never()).captureReplayEvent(any(), any(), any()) - } + fun `creating a transaction with origin sets the origin on the transaction context`() { + val scopes = generateScopes() - @Test - fun `when captureReplay is called with a valid argument, captureReplay on the client should be called`() { - val (sut, mockClient) = getEnabledHub() + val transactionContext = TransactionContext("transaction-name", "transaction-op") + val transactionOptions = TransactionOptions().also { + it.origin = "other.span.origin" + } - val event = SentryReplayEvent() - val hints = HintUtils.createWithTypeCheckHint({}) - sut.captureReplay(event, hints) - verify(mockClient).captureReplayEvent(eq(event), any(), eq(hints)) + val transaction = scopes.startTransaction(transactionContext, transactionOptions) + assertEquals("other.span.origin", transaction.spanContext.origin) } - // endregion replay event tests private val dsnTest = "https://key@sentry.io/proj" - private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration? = null): IHub { + private fun generateScopes(optionsConfiguration: Sentry.OptionsConfiguration? = null): IScopes { val options = SentryOptions().apply { dsn = dsnTest cacheDirPath = file.absolutePath @@ -2137,10 +2151,10 @@ class HubTest { tracesSampleRate = 1.0 } optionsConfiguration?.configure(options) - return Hub(options) + return createScopes(options) } - private fun getEnabledHub(): Triple { + private fun getEnabledScopes(): Triple { val logger = mock() val options = SentryOptions() @@ -2151,8 +2165,8 @@ class HubTest { options.isDebug = true options.setLogger(logger) - val sut = Hub(options) - val mockClient = mock() + val sut = createScopes(options) + val mockClient = createSentryClientMock() sut.bindClient(mockClient) return Triple(sut, mockClient, logger) } @@ -2173,8 +2187,7 @@ class HubTest { expectedContext.release == actual.release && expectedContext.publicKey == actual.publicKey && expectedContext.sampleRate == actual.sampleRate && - expectedContext.userId == actual.userId && - expectedContext.userSegment == actual.userSegment + expectedContext.userId == actual.userId } } } diff --git a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt index 78623f90a74..beed31a1981 100644 --- a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt @@ -19,7 +19,7 @@ import kotlin.test.assertTrue class SendCachedEnvelopeFireAndForgetIntegrationTest { private class Fixture { - var hub: IHub = mock() + var scopes: IScopes = mock() var logger: ILogger = mock() var options = SentryOptions() val sender = mock() @@ -45,7 +45,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fun `when cacheDirPath returns null, register logs and exit`() { fixture.options.cacheDirPath = null val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("No cache dir path is defined in options.")) verify(fixture.sender, never()).send() } @@ -73,7 +73,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { val sut = SendCachedEnvelopeFireAndForgetIntegration(CustomFactory()) fixture.options.cacheDirPath = "abc" fixture.options.executorService = ImmediateExecutorService() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("SendFireAndForget factory is null.")) verify(fixture.sender, never()).send() } @@ -85,7 +85,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { mock() ) val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) assertNotNull(fixture.options.sdkVersion) assert(fixture.options.sdkVersion!!.integrationSet.contains("SendCachedEnvelopeFireAndForget")) } @@ -96,7 +96,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.executorService.close(0) whenever(fixture.callback.create(any(), any())).thenReturn(mock()) val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("Failed to call the executor. Cached events will not be sent. Did you call Sentry.close()?"), any()) } @@ -108,7 +108,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(connectionStatusProvider).addConnectionStatusObserver(any()) } @@ -122,9 +122,9 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.sender, never()).send() } @@ -139,7 +139,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.sender).send() } @@ -155,7 +155,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // when there's no connection no factory create call should be done verify(fixture.sender, never()).send() @@ -183,9 +183,9 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { val rateLimiter = mock { whenever(mock.isActiveForCategory(any())).thenReturn(true) } - whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) // no factory call should be done if there's rate limiting active verify(fixture.sender, never()).send() @@ -196,8 +196,8 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.executorService = ImmediateExecutorService() fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) - verify(fixture.callback).create(eq(fixture.hub), eq(fixture.options)) + sut.register(fixture.scopes, fixture.options) + verify(fixture.callback).create(eq(fixture.scopes), eq(fixture.options)) } @Test @@ -205,7 +205,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.executorService = mock() fixture.options.cacheDirPath = "cache" val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.callback, never()).create(any(), any()) } @@ -215,7 +215,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.executorService = deferredExecutorService val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.sender, never()).send() sut.close() @@ -224,7 +224,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { } private class CustomFactory : SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { - override fun create(hub: IHub, options: SentryOptions): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { + override fun create(scopes: IScopes, options: SentryOptions): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { return null } } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index fb4f5ae8733..da57f5376e6 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -13,7 +13,6 @@ import io.sentry.hints.Backfillable import io.sentry.hints.Cached import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd -import io.sentry.metrics.NoopMetricsAggregator import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism import io.sentry.protocol.Request @@ -62,7 +61,6 @@ import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotEquals import kotlin.test.assertNotNull -import kotlin.test.assertNotSame import kotlin.test.assertNull import kotlin.test.assertSame import kotlin.test.assertTrue @@ -76,7 +74,7 @@ class SentryClientTest { var transport = mock() var factory = mock() val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() - val hub = mock() + val scopes = mock() val sentryTracer: SentryTracer var sentryOptions: SentryOptions = SentryOptions().apply { @@ -94,8 +92,8 @@ class SentryClientTest { init { whenever(factory.create(any(), any())).thenReturn(transport) - whenever(hub.options).thenReturn(sentryOptions) - sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), hub) + whenever(scopes.options).thenReturn(sentryOptions) + sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), scopes) sentryTracer.startChild("a-span", "span 1").finish() } @@ -589,7 +587,7 @@ class SentryClientTest { @Test fun `when captureCheckIn, envelope is sent if ignored slug does not match`() { val sut = fixture.getSut { options -> - options.ignoredCheckIns = listOf("non_matching_slug") + options.setIgnoredCheckIns(listOf("non_matching_slug")) } sut.captureCheckIn(checkIn, null, null) @@ -613,7 +611,7 @@ class SentryClientTest { @Test fun `when captureCheckIn, envelope is not sent if slug is ignored`() { val sut = fixture.getSut { options -> - options.ignoredCheckIns = listOf("some_slug") + options.setIgnoredCheckIns(listOf("some_slug")) } sut.captureCheckIn(checkIn, null, null) @@ -821,6 +819,50 @@ class SentryClientTest { ) } + @Test + fun `transaction dropped by ignoredTransactions is recorded`() { + fixture.sentryOptions.setIgnoredTransactions(listOf("a-transaction")) + + val transaction = SentryTransaction(fixture.sentryTracer) + + val eventId = + fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) + + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Transaction.category, 1), + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2) + ) + ) + + assertEquals(SentryId.EMPTY_ID, eventId) + } + + @Test + fun `transaction dropped by ignoredTransactions with regex is recorded`() { + fixture.sentryOptions.setIgnoredTransactions(listOf("a.*action")) + + val transaction = SentryTransaction(fixture.sentryTracer) + + val eventId = + fixture.getSut().captureTransaction(transaction, fixture.sentryTracer.traceContext()) + + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Transaction.category, 1), + DiscardedEvent(DiscardReason.EVENT_PROCESSOR.reason, DataCategory.Span.category, 2) + ) + ) + + assertEquals(SentryId.EMPTY_ID, eventId) + } + @Test fun `backfillable events are only wired through backfilling processors`() { val backfillingProcessor = mock() @@ -858,7 +900,7 @@ class SentryClientTest { environment = "release" release = "io.sentry.samples@22.1.1" contexts[Contexts.REPLAY_ID] = "64cf554cc8d74c6eafa3e08b7c984f6d" - contexts.trace = SpanContext(traceId, SpanId(), "ui.load", null, null) + contexts.setTrace(SpanContext(traceId, SpanId(), "ui.load", null, null)) transaction = "MainActivity" } val hint = HintUtils.createWithTypeCheckHint(BackfillableHint()) @@ -1530,9 +1572,9 @@ class SentryClientTest { @Test fun `when captureTransaction with scope, transaction should use user data`() { - val hub: IHub = mock() - whenever(hub.options).thenReturn(SentryOptions()) - val transaction = SentryTransaction(SentryTracer(TransactionContext("tx", "op"), hub)) + val scopes: IScopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) + val transaction = SentryTransaction(SentryTracer(TransactionContext("tx", "op"), scopes)) val scope = createScope() val sut = fixture.getSut() @@ -1561,7 +1603,7 @@ class SentryClientTest { val event = SentryEvent() val sut = fixture.getSut() val scope = createScope() - val transaction = SentryTracer(TransactionContext("a-transaction", "op"), fixture.hub) + val transaction = SentryTracer(TransactionContext("a-transaction", "op"), fixture.scopes) scope.setTransaction(transaction) val span = transaction.startChild("op") sut.captureEvent(event, scope) @@ -1632,7 +1674,7 @@ class SentryClientTest { fixture.sentryOptions.release = "optionsRelease" fixture.sentryOptions.environment = "optionsEnvironment" val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) transaction.release = "transactionRelease" transaction.environment = "transactionEnvironment" @@ -1645,7 +1687,7 @@ class SentryClientTest { fun `when transaction does not have SDK version set, and the SDK version is set on options, options values are applied to transactions`() { fixture.sentryOptions.sdkVersion = SdkVersion("sdk.name", "version") val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) sut.captureTransaction(transaction, sentryTracer.traceContext()) assertEquals(fixture.sentryOptions.sdkVersion, transaction.sdk) @@ -1655,7 +1697,7 @@ class SentryClientTest { fun `when transaction has SDK version set, and the SDK version is set on options, options values are not applied to transactions`() { fixture.sentryOptions.sdkVersion = SdkVersion("sdk.name", "version") val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) val sdkVersion = SdkVersion("transaction.sdk.name", "version") transaction.sdk = sdkVersion @@ -1667,7 +1709,7 @@ class SentryClientTest { fun `when transaction does not have tags, and tags are set on options, options values are applied to transactions`() { fixture.sentryOptions.setTag("tag1", "value1") val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) sut.captureTransaction(transaction, sentryTracer.traceContext()) assertEquals(mapOf("tag1" to "value1"), transaction.tags) @@ -1678,7 +1720,7 @@ class SentryClientTest { fixture.sentryOptions.setTag("tag1", "value1") fixture.sentryOptions.setTag("tag2", "value2") val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) transaction.setTag("tag3", "value3") transaction.setTag("tag2", "transaction-tag") @@ -1692,7 +1734,7 @@ class SentryClientTest { @Test fun `captured transactions without a platform, have the default platform set`() { val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) sut.captureTransaction(transaction, sentryTracer.traceContext()) assertEquals("java", transaction.platform) @@ -1701,7 +1743,7 @@ class SentryClientTest { @Test fun `captured transactions with a platform, do not get the platform overwritten`() { val sut = fixture.getSut() - val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.hub) + val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes) val transaction = SentryTransaction(sentryTracer) transaction.platform = "abc" sut.captureTransaction(transaction, sentryTracer.traceContext()) @@ -2496,7 +2538,7 @@ class SentryClientTest { val preExistingSpanContext = SpanContext("op.load") val sentryEvent = SentryEvent() - sentryEvent.contexts.trace = preExistingSpanContext + sentryEvent.contexts.setTrace(preExistingSpanContext) sut.captureEvent(sentryEvent, scope) verify(fixture.transport).send( @@ -2568,7 +2610,7 @@ class SentryClientTest { val preExistingSpanContext = SpanContext("op.load") val sentryEvent = SentryEvent() - sentryEvent.contexts.trace = preExistingSpanContext + sentryEvent.contexts.setTrace(preExistingSpanContext) sut.captureEvent(sentryEvent, scope) verify(fixture.transport).send( @@ -2606,22 +2648,6 @@ class SentryClientTest { verify(fixture.transport).send(anyOrNull(), anyOrNull()) } - @Test - fun `no-op metrics aggregator is returned when metrics is disabled`() { - val sut = fixture.getSut { options -> - options.isEnableMetrics = false - } - assertSame(NoopMetricsAggregator.getInstance(), sut.metricsAggregator) - } - - @Test - fun `metrics aggregator is returned when metrics is disabled`() { - val sut = fixture.getSut { options -> - options.isEnableMetrics = true - } - assertNotSame(NoopMetricsAggregator.getInstance(), sut.metricsAggregator) - } - @Test fun `when captureReplayEvent, envelope is sent`() { val sut = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt index 888f17e0a3e..8eb7f0e42d1 100644 --- a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt @@ -209,6 +209,193 @@ class SentryExceptionFactoryTest { assertEquals(777, frame.lineno) } + @Test + fun `when exception with mechanism suppressed exceptions, add them and show as group`() { + val exception = Exception("message") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val mechanism = Mechanism() + mechanism.type = "ANR" + val thread = Thread() + val throwable = ExceptionMechanismException(mechanism, exception, thread) + + val queue = fixture.getSut().extractExceptionQueue(throwable) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(1, suppressedInQueue.mechanism?.exceptionId) + assertEquals(0, suppressedInQueue.mechanism?.parentId) + + assertEquals("message", mainInQueue.value) + assertEquals(0, mainInQueue.mechanism?.exceptionId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception that contains suppressed exceptions is marked as group`() { + val exception = Exception("inner") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val outerException = Exception("outer", exception) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(2, suppressedInQueue.mechanism?.exceptionId) + assertEquals(1, suppressedInQueue.mechanism?.parentId) + + assertEquals("inner", mainInQueue.value) + assertEquals(1, mainInQueue.mechanism?.exceptionId) + assertEquals(0, mainInQueue.mechanism?.parentId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception within Mechanism that contains suppressed exceptions is marked as group`() { + val exception = Exception("inner") + val suppressedException = Exception("suppressed") + exception.addSuppressed(suppressedException) + + val mechanism = Mechanism() + mechanism.type = "ANR" + val thread = Thread() + + val outerException = ExceptionMechanismException(mechanism, Exception("outer", exception), thread) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val suppressedInQueue = queue.pop() + val mainInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("suppressed", suppressedInQueue.value) + assertEquals(2, suppressedInQueue.mechanism?.exceptionId) + assertEquals(1, suppressedInQueue.mechanism?.parentId) + + assertEquals("inner", mainInQueue.value) + assertEquals(1, mainInQueue.mechanism?.exceptionId) + assertEquals(0, mainInQueue.mechanism?.parentId) + assertEquals(true, mainInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception with nested exception that contain suppressed exceptions are marked as group`() { + val innerMostException = Exception("innermost") + val innerMostSuppressed = Exception("innermostSuppressed") + innerMostException.addSuppressed(innerMostSuppressed) + + val innerException = Exception("inner", innerMostException) + val innerSuppressed = Exception("suppressed") + innerException.addSuppressed(innerSuppressed) + + val outerException = Exception("outer", innerException) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val innerMostSuppressedInQueue = queue.pop() + val innerMostExceptionInQueue = queue.pop() + val innerSuppressedInQueue = queue.pop() + val innerExceptionInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value) + assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId) + assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermost", innerMostExceptionInQueue.value) + assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId) + assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("suppressed", innerSuppressedInQueue.value) + assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId) + assertEquals(1, innerSuppressedInQueue.mechanism?.parentId) + assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("inner", innerExceptionInQueue.value) + assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId) + assertEquals(0, innerExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + + @Test + fun `nested exception with nested exception that contain suppressed exceptions with a nested exception are marked as group`() { + val innerMostException = Exception("innermost") + + val innerMostSuppressedNestedException = Exception("innermostSuppressedNested") + val innerMostSuppressed = Exception("innermostSuppressed", innerMostSuppressedNestedException) + innerMostException.addSuppressed(innerMostSuppressed) + + val innerException = Exception("inner", innerMostException) + val innerSuppressed = Exception("suppressed") + innerException.addSuppressed(innerSuppressed) + + val outerException = Exception("outer", innerException) + + val queue = fixture.getSut().extractExceptionQueue(outerException) + + val innerMostSuppressedNestedExceptionInQueue = queue.pop() + val innerMostSuppressedInQueue = queue.pop() + val innerMostExceptionInQueue = queue.pop() + val innerSuppressedInQueue = queue.pop() + val innerExceptionInQueue = queue.pop() + val outerInQueue = queue.pop() + + assertEquals("innermostSuppressedNested", innerMostSuppressedNestedExceptionInQueue.value) + assertEquals(5, innerMostSuppressedNestedExceptionInQueue.mechanism?.exceptionId) + assertEquals(4, innerMostSuppressedNestedExceptionInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedNestedExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermostSuppressed", innerMostSuppressedInQueue.value) + assertEquals(4, innerMostSuppressedInQueue.mechanism?.exceptionId) + assertEquals(3, innerMostSuppressedInQueue.mechanism?.parentId) + assertNull(innerMostSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("innermost", innerMostExceptionInQueue.value) + assertEquals(3, innerMostExceptionInQueue.mechanism?.exceptionId) + assertEquals(1, innerMostExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerMostExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("suppressed", innerSuppressedInQueue.value) + assertEquals(2, innerSuppressedInQueue.mechanism?.exceptionId) + assertEquals(1, innerSuppressedInQueue.mechanism?.parentId) + assertNull(innerSuppressedInQueue.mechanism?.isExceptionGroup) + + assertEquals("inner", innerExceptionInQueue.value) + assertEquals(1, innerExceptionInQueue.mechanism?.exceptionId) + assertEquals(0, innerExceptionInQueue.mechanism?.parentId) + assertEquals(true, innerExceptionInQueue.mechanism?.isExceptionGroup) + + assertEquals("outer", outerInQueue.value) + assertEquals(0, outerInQueue.mechanism?.exceptionId) + assertNull(outerInQueue.mechanism?.parentId) + assertNull(outerInQueue.mechanism?.isExceptionGroup) + } + internal class InnerClassThrowable constructor(cause: Throwable? = null) : Throwable(cause) private val anonymousException = object : Exception() { diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index b474d4e4e03..46482a10833 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.SentryOptions.RequestSize import io.sentry.util.StringUtils import org.mockito.kotlin.mock import java.io.File @@ -12,7 +13,6 @@ import kotlin.test.assertIs import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull -import kotlin.test.assertSame import kotlin.test.assertTrue class SentryOptionsTest { @@ -130,15 +130,6 @@ class SentryOptionsTest { assertTrue(options.isTracingEnabled) } - @Test - fun `when enableTracing is set to true tracing is considered enabled`() { - val options = SentryOptions().apply { - this.enableTracing = true - } - - assertTrue(options.isTracingEnabled) - } - @Test fun `by default tracing is considered disabled`() { val options = SentryOptions() @@ -146,17 +137,6 @@ class SentryOptionsTest { assertFalse(options.isTracingEnabled) } - @Test - fun `when enableTracing is set to false tracing is considered disabled`() { - val options = SentryOptions().apply { - this.enableTracing = false - this.tracesSampleRate = 1.0 - this.tracesSampler = SentryOptions.TracesSamplerCallback { _ -> 1.0 } - } - - assertFalse(options.isTracingEnabled) - } - @Test fun `when there's no cacheDirPath, outboxPath returns null`() { val options = SentryOptions() @@ -269,30 +249,6 @@ class SentryOptionsTest { assertFailsWith { SentryOptions().profilesSampleRate = -0.0000000000001 } } - @Test - fun `when profilingEnabled is set to true, profilesSampleRate is set to 1`() { - val options = SentryOptions() - options.isProfilingEnabled = true - assertEquals(1.0, options.profilesSampleRate) - } - - @Test - fun `when profilingEnabled is set to false, profilesSampleRate is set to null`() { - val options = SentryOptions() - options.isProfilingEnabled = false - assertNull(options.profilesSampleRate) - } - - @Test - fun `when profilesSampleRate is set, setting profilingEnabled is ignored`() { - val options = SentryOptions() - options.profilesSampleRate = 0.2 - options.isProfilingEnabled = true - assertEquals(0.2, options.profilesSampleRate) - options.isProfilingEnabled = false - assertEquals(0.2, options.profilesSampleRate) - } - @Test fun `when options is initialized, transactionPerformanceCollector is set`() { assertIs(SentryOptions().transactionPerformanceCollector) @@ -354,7 +310,6 @@ class SentryOptionsTest { externalOptions.setTag("tag1", "value1") externalOptions.setTag("tag2", "value2") externalOptions.enableUncaughtExceptionHandler = false - externalOptions.enableTracing = true externalOptions.tracesSampleRate = 0.5 externalOptions.profilesSampleRate = 0.5 externalOptions.addInAppInclude("com.app") @@ -370,7 +325,11 @@ class SentryOptionsTest { externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") + externalOptions.ignoredTransactions = listOf("transactionName1", "transaction-name-B") externalOptions.isEnableBackpressureHandling = false + externalOptions.maxRequestBodySize = SentryOptions.RequestSize.MEDIUM + externalOptions.isSendDefaultPii = true + externalOptions.isForceInit = true externalOptions.cron = SentryOptions.Cron().apply { defaultCheckinMargin = 10L defaultMaxRuntime = 30L @@ -378,6 +337,9 @@ class SentryOptionsTest { defaultFailureIssueThreshold = 40L defaultRecoveryThreshold = 50L } + externalOptions.isEnableSpotlight = true + externalOptions.spotlightConnectionUrl = "http://local.sentry.io:1234" + externalOptions.isGlobalHubMode = true val options = SentryOptions() @@ -394,7 +356,6 @@ class SentryOptionsTest { assertEquals(java.net.Proxy.Type.SOCKS, options.proxy!!.type) assertEquals(mapOf("tag1" to "value1", "tag2" to "value2"), options.tags) assertFalse(options.isEnableUncaughtExceptionHandler) - assertEquals(true, options.enableTracing) assertEquals(0.5, options.tracesSampleRate) assertEquals(0.5, options.profilesSampleRate) assertEquals(listOf("com.app"), options.inAppIncludes) @@ -407,14 +368,21 @@ class SentryOptionsTest { assertFalse(options.isEnabled) assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) - assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) + assertEquals(listOf(FilterString("slug1"), FilterString("slug-B")), options.ignoredCheckIns) + assertEquals(listOf(FilterString("transactionName1"), FilterString("transaction-name-B")), options.ignoredTransactions) assertFalse(options.isEnableBackpressureHandling) + assertTrue(options.isForceInit) assertNotNull(options.cron) assertEquals(10L, options.cron?.defaultCheckinMargin) assertEquals(30L, options.cron?.defaultMaxRuntime) assertEquals(40L, options.cron?.defaultFailureIssueThreshold) assertEquals(50L, options.cron?.defaultRecoveryThreshold) assertEquals("America/New_York", options.cron?.defaultTimezone) + assertTrue(options.isSendDefaultPii) + assertEquals(RequestSize.MEDIUM, options.maxRequestBodySize) + assertTrue(options.isEnableSpotlight) + assertEquals("http://local.sentry.io:1234", options.spotlightConnectionUrl) + assertTrue(options.isGlobalHubMode!!) } @Test @@ -568,11 +536,26 @@ class SentryOptionsTest { assertTrue(SentryOptions().isEnableBackpressureHandling) } + @Test + fun `when options are initialized, enableSpotlight is set to false by default`() { + assertFalse(SentryOptions().isEnableSpotlight) + } + + @Test + fun `when options are initialized, spotlightConnectionUrl is not set by default`() { + assertNull(SentryOptions().spotlightConnectionUrl) + } + @Test fun `when options are initialized, enableAppStartProfiling is set to false by default`() { assertFalse(SentryOptions().isEnableAppStartProfiling) } + @Test + fun `when options are initialized, isGlobalHubMode is set to null by default`() { + assertNull(SentryOptions().isGlobalHubMode) + } + @Test fun `when setEnableAppStartProfiling is called, overrides default`() { val options = SentryOptions() @@ -623,49 +606,6 @@ class SentryOptionsTest { assertEquals(true, SentryOptions().isEnableScopePersistence) } - @Test - fun `when options are initialized, metrics is disabled by default`() { - assertFalse(SentryOptions().isEnableMetrics) - assertFalse(SentryOptions().isEnableDefaultTagsForMetrics) - assertFalse(SentryOptions().isEnableSpanLocalMetricAggregation) - } - - @Test - fun `when metrics is enabled, getters reflect that`() { - val options = SentryOptions().apply { - isEnableMetrics = true - } - assertTrue(options.isEnableMetrics) - assertTrue(options.isEnableDefaultTagsForMetrics) - assertTrue(options.isEnableSpanLocalMetricAggregation) - } - - @Test - fun `when metric settings are flipped, getters reflect that`() { - val options = SentryOptions().apply { - isEnableMetrics = true - isEnableDefaultTagsForMetrics = false - isEnableSpanLocalMetricAggregation = false - } - assertTrue(options.isEnableMetrics) - assertFalse(options.isEnableDefaultTagsForMetrics) - assertFalse(options.isEnableSpanLocalMetricAggregation) - } - - @Test - fun `metric callback is null by default`() { - assertNull(SentryOptions().beforeEmitMetricCallback) - } - - @Test - fun `when metric callback is set, getter returns it`() { - val callback = SentryOptions.BeforeEmitMetricCallback { _, _ -> false } - val options = SentryOptions().apply { - beforeEmitMetricCallback = callback - } - assertSame(callback, options.beforeEmitMetricCallback) - } - @Test fun `existing cron defaults are not overridden if not present in external options`() { val options = SentryOptions().apply { @@ -721,4 +661,9 @@ class SentryOptionsTest { assertEquals(30, options.cron?.defaultFailureIssueThreshold) assertEquals(40, options.cron?.defaultRecoveryThreshold) } + + @Test + fun `when options is initialized, InitPriority is set to MEDIUM by default`() { + assertEquals(SentryOptions().initPriority, InitPriority.MEDIUM) + } } diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTracingTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTracingTest.kt index 469608ec2ca..5ff3e8c2445 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTracingTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTracingTest.kt @@ -5,7 +5,7 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import kotlin.test.assertEquals -data class TracingEnabledTestData(val enableTracing: Boolean?, val tracesSampleRate: Double?, val tracesSamplerPresent: Boolean, val isTracingEnabled: Boolean) +data class TracingEnabledTestData(val tracesSampleRate: Double?, val tracesSamplerPresent: Boolean, val isTracingEnabled: Boolean) /** * Test @link{SentryOptions#isTracingEnabled()} with combination of other options. @@ -18,25 +18,12 @@ class SentryOptionsTracingTest(private val testData: TracingEnabledTestData) { @Parameterized.Parameters fun data(): Collection> { return listOf( - TracingEnabledTestData(null, null, false, false), - TracingEnabledTestData(null, 1.0, false, true), - TracingEnabledTestData(false, 1.0, false, false), - TracingEnabledTestData(true, 1.0, false, true), - TracingEnabledTestData(null, 0.0, false, true), - TracingEnabledTestData(false, 0.0, false, false), - TracingEnabledTestData(true, 0.0, false, true), - TracingEnabledTestData(true, null, false, true), - TracingEnabledTestData(false, null, false, false), - - TracingEnabledTestData(null, null, true, true), - TracingEnabledTestData(null, 1.0, true, true), - TracingEnabledTestData(false, 1.0, true, false), - TracingEnabledTestData(true, 1.0, true, true), - TracingEnabledTestData(null, 0.0, true, true), - TracingEnabledTestData(false, 0.0, true, false), - TracingEnabledTestData(true, 0.0, true, true), - TracingEnabledTestData(true, null, true, true), - TracingEnabledTestData(false, null, true, false) + TracingEnabledTestData(null, false, false), + TracingEnabledTestData(1.0, false, true), + TracingEnabledTestData(0.0, false, true), + TracingEnabledTestData(null, true, true), + TracingEnabledTestData(1.0, true, true), + TracingEnabledTestData(0.0, true, true) ).map { arrayOf(it) } } } @@ -44,7 +31,6 @@ class SentryOptionsTracingTest(private val testData: TracingEnabledTestData) { @Test fun `test isTracingEnabled`() { val options = SentryOptions().apply { - testData.enableTracing?.let { this.enableTracing = it } testData.tracesSampleRate?.let { this.tracesSampleRate = it } if (testData.tracesSamplerPresent) { this.tracesSampler = SentryOptions.TracesSamplerCallback { samplingContext -> 1.0 } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 697450f0e50..28c2fe3367b 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -15,9 +15,12 @@ import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId import io.sentry.protocol.SentryThread import io.sentry.test.ImmediateExecutorService +import io.sentry.test.createSentryClientMock +import io.sentry.test.initForTest +import io.sentry.test.injectForField import io.sentry.util.PlatformTestManipulator -import io.sentry.util.thread.IMainThreadChecker -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.IThreadChecker +import io.sentry.util.thread.ThreadChecker import org.awaitility.kotlin.await import org.junit.Assert.assertThrows import org.junit.Rule @@ -31,6 +34,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.io.Closeable import java.io.File import java.io.FileReader import java.nio.file.Files @@ -41,11 +45,14 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFails import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotEquals import kotlin.test.assertNotNull +import kotlin.test.assertNotSame import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue class SentryTest { @@ -63,33 +70,109 @@ class SentryTest { } @Test - fun `init multiple times calls hub close with isRestarting true`() { - val hub = mock() - Sentry.init { + fun `init multiple times calls scopes close with isRestarting true`() { + val scopes = mock() + initForTest { it.dsn = dsn } - Sentry.setCurrentHub(hub) - Sentry.init { + Sentry.setCurrentScopes(scopes) + initForTest { it.dsn = dsn } - verify(hub).close(eq(true)) + verify(scopes).close(eq(true)) } @Test - fun `close calls hub close with isRestarting false`() { - val hub = mock() - Sentry.init { + fun `init multiple times calls close on previous options not new`() { + val profiler1 = mock() + val profiler2 = mock() + initForTest { it.dsn = dsn + it.setTransactionProfiler(profiler1) } - Sentry.setCurrentHub(hub) + verify(profiler1, never()).close() + + initForTest { + it.dsn = dsn + it.setTransactionProfiler(profiler2) + } + verify(profiler2, never()).close() + verify(profiler1).close() + + Sentry.close() + verify(profiler2).close() + } + + @Test + fun `init multiple times calls close on previous integrations not new`() { + val integration1 = mock() + val integration2 = mock() + initForTest { + it.dsn = dsn + it.addIntegration(integration1) + } + verify(integration1, never()).close() + + initForTest { + it.dsn = dsn + it.addIntegration(integration2) + } + verify(integration2, never()).close() + verify(integration1).close() + + Sentry.close() + verify(integration2).close() + } + + interface CloseableIntegration : Integration, Closeable + + @Test + fun `global client is enabled after restart`() { + val scopes = mock() + whenever(scopes.close()).then { Sentry.getGlobalScope().client.close() } + whenever(scopes.close(anyOrNull())).then { Sentry.getGlobalScope().client.close() } + + initForTest { + it.dsn = dsn + } + Sentry.setCurrentScopes(scopes) + initForTest { + it.dsn = dsn + } + verify(scopes).close(eq(true)) + assertTrue(Sentry.getGlobalScope().client.isEnabled) + } + + @Test + fun `global client is disabled after close`() { + val scopes = mock() + whenever(scopes.close()).then { Sentry.getGlobalScope().client.close() } + whenever(scopes.close(anyOrNull())).then { Sentry.getGlobalScope().client.close() } + + initForTest { + it.dsn = dsn + } + Sentry.setCurrentScopes(scopes) + Sentry.close() + verify(scopes).close(eq(false)) + assertFalse(Sentry.getGlobalScope().client.isEnabled) + } + + @Test + fun `close calls scopes close with isRestarting false`() { + val scopes = mock() + initForTest { + it.dsn = dsn + } + Sentry.setCurrentScopes(scopes) Sentry.close() - verify(hub).close(eq(false)) + verify(scopes).close(eq(false)) } @Test fun `outboxPath should be created at initialization`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() sentryOptions = it @@ -103,7 +186,7 @@ class SentryTest { @Test fun `cacheDirPath should be created at initialization`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() sentryOptions = it @@ -117,7 +200,7 @@ class SentryTest { @Test fun `getCacheDirPathWithoutDsn should be created at initialization`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() sentryOptions = it @@ -132,7 +215,7 @@ class SentryTest { @Test fun `Init sets SystemOutLogger if logger is NoOp and debug is enabled`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() sentryOptions = it @@ -144,7 +227,7 @@ class SentryTest { @Test fun `scope changes are isolated to a thread`() { - Sentry.init { + initForTest { it.dsn = dsn } Sentry.configureScope { @@ -169,10 +252,10 @@ class SentryTest { @Test fun `warns about multiple Sentry initializations`() { val logger = mock() - Sentry.init { + initForTest { it.dsn = dsn } - Sentry.init { + initForTest { it.dsn = dsn it.setDebug(true) it.setLogger(logger) @@ -186,8 +269,8 @@ class SentryTest { @Test fun `warns about multiple Sentry initializations with string overload`() { val logger = mock() - Sentry.init(dsn) - Sentry.init { + initForTest(dsn) + initForTest { it.dsn = dsn it.setDebug(true) it.setLogger(logger) @@ -210,10 +293,10 @@ class SentryTest { try { // initialize Sentry with empty DSN and enable loading properties from external sources - Sentry.init { + initForTest { it.isEnableExternalConfiguration = true } - assertTrue(HubAdapter.getInstance().isEnabled) + assertTrue(ScopesAdapter.getInstance().isEnabled) } finally { temporaryFolder.delete() } @@ -221,7 +304,7 @@ class SentryTest { @Test fun `initializes Sentry with enabled=false, thus disabling Sentry even if dsn is set`() { - Sentry.init { + initForTest { it.isEnabled = false it.dsn = "http://key@localhost/proj" } @@ -229,33 +312,33 @@ class SentryTest { Sentry.setTag("none", "shouldNotExist") var value: String? = null - Sentry.getCurrentHub().configureScope { + Sentry.getCurrentScopes().configureScope { value = it.tags[value] } - assertTrue(Sentry.getCurrentHub() is NoOpHub) + assertTrue(Sentry.getCurrentScopes().isNoOp) assertNull(value) } @Test fun `initializes Sentry with enabled=false, thus disabling Sentry even if dsn is null`() { - Sentry.init { + initForTest { it.isEnabled = false } Sentry.setTag("none", "shouldNotExist") var value: String? = null - Sentry.getCurrentHub().configureScope { + Sentry.getCurrentScopes().configureScope { value = it.tags[value] } - assertTrue(Sentry.getCurrentHub() is NoOpHub) + assertTrue(Sentry.getCurrentScopes().isNoOp) assertNull(value) } @Test fun `initializes Sentry with dsn = null, throwing IllegalArgumentException`() { val exception = - assertThrows(java.lang.IllegalArgumentException::class.java) { Sentry.init() } + assertThrows(java.lang.IllegalArgumentException::class.java) { initForTest() } assertEquals( "DSN is required. Use empty string or set enabled to false in SentryOptions to disable SDK.", exception.message @@ -264,10 +347,10 @@ class SentryTest { @Test fun `captureUserFeedback gets forwarded to client`() { - Sentry.init { it.dsn = dsn } + initForTest { it.dsn = dsn } - val client = mock() - Sentry.getCurrentHub().bindClient(client) + val client = createSentryClientMock() + Sentry.getCurrentScopes().bindClient(client) val userFeedback = UserFeedback(SentryId.EMPTY_ID) Sentry.captureUserFeedback(userFeedback) @@ -281,7 +364,7 @@ class SentryTest { @Test fun `startTransaction sets operation and description`() { - Sentry.init { + initForTest { it.dsn = dsn it.tracesSampleRate = 1.0 } @@ -294,7 +377,7 @@ class SentryTest { @Test fun `isCrashedLastRun returns true if crashedLastRun is set`() { - Sentry.init { + initForTest { it.dsn = dsn } @@ -307,7 +390,7 @@ class SentryTest { fun `profilingTracesDirPath should be created and cleared at initialization when profiling is enabled`() { val tempPath = getTempPath() var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.profilesSampleRate = 1.0 it.cacheDirPath = tempPath @@ -342,7 +425,7 @@ class SentryTest { assertTrue(oldProfile.exists()) assertTrue(newProfile.exists()) - Sentry.init { + initForTest { it.dsn = dsn it.profilesSampleRate = 1.0 it.cacheDirPath = tempPath @@ -358,7 +441,7 @@ class SentryTest { fun `profilingTracesDirPath should not be created and cleared when profiling is disabled`() { val tempPath = getTempPath() var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.profilesSampleRate = 0.0 it.cacheDirPath = tempPath @@ -369,15 +452,15 @@ class SentryTest { } @Test - fun `using sentry before calling init creates NoOpHub but after init Sentry uses a new clone`() { - // noop as not yet initialized, caches NoOpHub in ThreadLocal + fun `using sentry before calling init creates NoOpScopes but after init Sentry uses a new clone`() { + // noop as not yet initialized, caches NoOpScopes in ThreadLocal Sentry.captureMessage("noop caused") - assertTrue(Sentry.getCurrentHub() is NoOpHub) + assertTrue(Sentry.getCurrentScopes().isNoOp) // init Sentry in another thread val thread = Thread() { - Sentry.init { + initForTest { it.dsn = dsn it.isDebug = true } @@ -387,24 +470,24 @@ class SentryTest { Sentry.captureMessage("should work now") - val hub = Sentry.getCurrentHub() - assertNotNull(hub) - assertFalse(hub is NoOpHub) + val scopes = Sentry.getCurrentScopes() + assertNotNull(scopes) + assertFalse(scopes.isNoOp) } @Test - fun `main hub can be cloned and does not share scope with current hub`() { - // noop as not yet initialized, caches NoOpHub in ThreadLocal + fun `main scopes can be cloned and does not share scope with current scopes`() { + // noop as not yet initialized, caches NoOpScopes in ThreadLocal Sentry.addBreadcrumb("breadcrumbNoOp") Sentry.captureMessage("messageNoOp") - assertTrue(Sentry.getCurrentHub() is NoOpHub) + assertTrue(Sentry.getCurrentScopes().isNoOp) val capturedEvents = mutableListOf() // init Sentry in another thread val thread = Thread() { - Sentry.init { + initForTest { it.dsn = dsn it.isDebug = true it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> @@ -418,44 +501,44 @@ class SentryTest { Sentry.addBreadcrumb("breadcrumbCurrent") - val hub = Sentry.getCurrentHub() - assertNotNull(hub) - assertFalse(hub is NoOpHub) + val scopes = Sentry.getCurrentScopes() + assertNotNull(scopes) + assertFalse(Sentry.getCurrentScopes().isNoOp) - val newMainHubClone = Sentry.cloneMainHub() - newMainHubClone.addBreadcrumb("breadcrumbMainClone") + val forkedRootScopes = Sentry.forkedRootScopes("test") + forkedRootScopes.addBreadcrumb("breadcrumbMainClone") - hub.captureMessage("messageCurrent") - newMainHubClone.captureMessage("messageMainClone") + scopes.captureMessage("messageCurrent") + forkedRootScopes.captureMessage("messageMainClone") assertEquals(2, capturedEvents.size) val mainCloneEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageMainClone" } - val currentHubEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageCurrent" } + val currentScopesEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageCurrent" } assertNotNull(mainCloneEvent) assertNotNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) assertNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) assertNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) - assertNotNull(currentHubEvent) - assertNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) - assertNotNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) - assertNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) + assertNotNull(currentScopesEvent) + assertNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) + assertNotNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) + assertNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) } @Test - fun `main hub is not cloned in global hub mode and shares scope with current hub`() { - // noop as not yet initialized, caches NoOpHub in ThreadLocal + fun `main scopes is not cloned in global scopes mode and shares scope with current scopes`() { + // noop as not yet initialized, caches NoOpScopes in ThreadLocal Sentry.addBreadcrumb("breadcrumbNoOp") Sentry.captureMessage("messageNoOp") - assertTrue(Sentry.getCurrentHub() is NoOpHub) + assertTrue(Sentry.getCurrentScopes().isNoOp) val capturedEvents = mutableListOf() // init Sentry in another thread val thread = Thread() { - Sentry.init({ + initForTest({ it.dsn = dsn it.isDebug = true it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> @@ -469,29 +552,29 @@ class SentryTest { Sentry.addBreadcrumb("breadcrumbCurrent") - val hub = Sentry.getCurrentHub() - assertNotNull(hub) - assertFalse(hub is NoOpHub) + val scopes = Sentry.getCurrentScopes() + assertNotNull(scopes) + assertFalse(scopes.isNoOp) - val newMainHubClone = Sentry.cloneMainHub() - newMainHubClone.addBreadcrumb("breadcrumbMainClone") + val forkedRootScopes = Sentry.forkedRootScopes("test") + forkedRootScopes.addBreadcrumb("breadcrumbMainClone") - hub.captureMessage("messageCurrent") - newMainHubClone.captureMessage("messageMainClone") + scopes.captureMessage("messageCurrent") + forkedRootScopes.captureMessage("messageMainClone") assertEquals(2, capturedEvents.size) val mainCloneEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageMainClone" } - val currentHubEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageCurrent" } + val currentScopesEvent = capturedEvents.firstOrNull { it.message?.formatted == "messageCurrent" } assertNotNull(mainCloneEvent) assertNotNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) assertNotNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) assertNull(mainCloneEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) - assertNotNull(currentHubEvent) - assertNotNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) - assertNotNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) - assertNull(currentHubEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) + assertNotNull(currentScopesEvent) + assertNotNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbMainClone" }) + assertNotNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbCurrent" }) + assertNull(currentScopesEvent.breadcrumbs?.firstOrNull { it.message == "breadcrumbNoOp" }) } @Test @@ -499,7 +582,7 @@ class SentryTest { val logger = mock() val initException = Exception("init") - Sentry.init({ + initForTest({ it.dsn = dsn it.isDebug = true it.setLogger(logger) @@ -530,7 +613,7 @@ class SentryTest { fun `overrides envelope cache if it's not set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() sentryOptions = it @@ -543,7 +626,7 @@ class SentryTest { fun `does not override envelope cache if it's already set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = getTempPath() it.setEnvelopeDiskCache(CustomEnvelopCache()) @@ -557,7 +640,7 @@ class SentryTest { fun `overrides modules loader if it's not set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn sentryOptions = it } @@ -569,7 +652,7 @@ class SentryTest { fun `does not override modules loader if it's already set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.setModulesLoader(CustomModulesLoader()) sentryOptions = it @@ -582,7 +665,7 @@ class SentryTest { fun `overrides debug meta loader if it's not set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn sentryOptions = it } @@ -594,7 +677,7 @@ class SentryTest { fun `does not override debug meta loader if it's already set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.setDebugMetaLoader(CustomDebugMetaLoader()) sentryOptions = it @@ -607,32 +690,32 @@ class SentryTest { fun `overrides main thread checker if it's not set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn sentryOptions = it } - assertTrue { sentryOptions!!.mainThreadChecker is MainThreadChecker } + assertTrue { sentryOptions!!.threadChecker is ThreadChecker } } @Test fun `does not override main thread checker if it's already set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn - it.mainThreadChecker = CustomMainThreadChecker() + it.threadChecker = CustomThreadChecker() sentryOptions = it } - assertTrue { sentryOptions!!.mainThreadChecker is CustomMainThreadChecker } + assertTrue { sentryOptions!!.threadChecker is CustomThreadChecker } } @Test fun `overrides collector if it's not set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn sentryOptions = it } @@ -644,7 +727,7 @@ class SentryTest { fun `does not override collector if it's already set`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.addPerformanceCollector(CustomMemoryCollector()) sentryOptions = it @@ -657,7 +740,7 @@ class SentryTest { fun `init does not throw on executor shut down`() { val logger = mock() - Sentry.init { + initForTest { it.dsn = dsn it.profilesSampleRate = 1.0 it.cacheDirPath = getTempPath() @@ -669,25 +752,14 @@ class SentryTest { } @Test - fun `reportFullyDisplayed calls hub reportFullyDisplayed`() { - val hub = mock() - Sentry.init { + fun `reportFullyDisplayed calls scopes reportFullyDisplayed`() { + val scopes = mock() + initForTest { it.dsn = dsn } - Sentry.setCurrentHub(hub) + Sentry.setCurrentScopes(scopes) Sentry.reportFullyDisplayed() - verify(hub).reportFullyDisplayed() - } - - @Test - fun `reportFullDisplayed calls reportFullyDisplayed`() { - val hub = mock() - Sentry.init { - it.dsn = dsn - } - Sentry.setCurrentHub(hub) - Sentry.reportFullDisplayed() - verify(hub).reportFullyDisplayed() + verify(scopes).reportFullyDisplayed() } @Test @@ -696,7 +768,7 @@ class SentryTest { val executorService = mock() whenever(executorService.isClosed).thenReturn(true) - Sentry.init { + initForTest { it.dsn = dsn it.executorService = executorService sentryOptions = it @@ -711,7 +783,7 @@ class SentryTest { val executorService = mock() whenever(executorService.isClosed).thenReturn(false) - Sentry.init { + initForTest { it.dsn = dsn it.executorService = executorService sentryOptions = it @@ -724,7 +796,7 @@ class SentryTest { fun `init notifies option observers`() { val optionsObserver = InMemoryOptionsObserver() - Sentry.init { + initForTest { it.dsn = dsn it.executorService = ImmediateExecutorService() @@ -757,7 +829,7 @@ class SentryTest { } val triggered = AtomicBoolean(false) - Sentry.init { + initForTest { it.dsn = dsn it.addOptionsObserver(optionsObserver) @@ -787,7 +859,7 @@ class SentryTest { fun `init finalizes previous session`() { lateinit var previousSessionFile: File - Sentry.init { + initForTest { it.dsn = dsn it.isDebug = true it.setLogger(SystemOutLogger()) @@ -808,11 +880,11 @@ class SentryTest { it.serializer.deserialize(previousSessionFile.bufferedReader(), Session::class.java)!!.environment ) - it.addIntegration { hub, _ -> + it.addIntegration { scopes, _ -> // this is just a hack to trigger the previousSessionFlush latch, so the finalizer // does not time out waiting. We have to do it as integration, because this is where - // the hub is already initialized - hub.startSession() + // the scopes is already initialized + scopes.startSession() } } @@ -824,7 +896,7 @@ class SentryTest { lateinit var previousSessionFile: File val triggered = AtomicBoolean(false) - Sentry.init { + initForTest { it.dsn = dsn it.release = "io.sentry.sample@2.0" @@ -860,10 +932,10 @@ class SentryTest { @Test fun `captureCheckIn gets forwarded to client`() { - Sentry.init { it.dsn = dsn } + initForTest { it.dsn = dsn } - val client = mock() - Sentry.getCurrentHub().bindClient(client) + val client = createSentryClientMock() + Sentry.getCurrentScopes().bindClient(client) val checkIn = CheckIn("some_slug", CheckInStatus.OK) Sentry.captureCheckIn(checkIn) @@ -880,7 +952,7 @@ class SentryTest { @Test fun `if send modules is false, uses NoOpModulesLoader`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.isSendModules = false sentryOptions = it @@ -891,7 +963,7 @@ class SentryTest { @Test fun `if Sentry is disabled through options with scope callback is executed`() { - Sentry.init { + initForTest { it.isEnabled = false } @@ -912,24 +984,30 @@ class SentryTest { } @Test - fun `getSpan calls hub getSpan`() { - val hub = mock() - Sentry.init({ - it.dsn = dsn - }, false) - Sentry.setCurrentHub(hub) + fun `getSpan calls scopes getSpan`() { + val scopes = mock() + val options = SentryOptions().also { it.dsn = dsn } + whenever(scopes.options).thenReturn(options) + + initForTest(options) + + Sentry.setCurrentScopes(scopes) Sentry.getSpan() - verify(hub).span + verify(scopes).span } @Test - fun `getSpan calls returns root span if globalhub mode is enabled on Android`() { + fun `getSpan calls returns root span if globalHubMode is enabled on Android`() { + var sentryOptions: CustomAndroidOptions? = null PlatformTestManipulator.pretendIsAndroid(true) - Sentry.init({ + Sentry.init(OptionsContainer.create(CustomAndroidOptions::class.java), { it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.sampleRate = 1.0 + it.mockName() + sentryOptions = it }, true) + sentryOptions?.resetName() val transaction = Sentry.startTransaction("name", "op-root", TransactionOptions().also { it.isBindToScope = true }) transaction.startChild("op-child") @@ -940,11 +1018,11 @@ class SentryTest { } @Test - fun `getSpan calls returns child span if globalhub mode is enabled, but the platform is not Android`() { + fun `getSpan calls returns child span if globalHubMode is enabled, but the platform is not Android`() { PlatformTestManipulator.pretendIsAndroid(false) - Sentry.init({ + initForTest({ it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.sampleRate = 1.0 }, false) @@ -956,10 +1034,10 @@ class SentryTest { } @Test - fun `getSpan calls returns child span if globalhub mode is disabled`() { - Sentry.init({ + fun `getSpan calls returns child span if globalHubMode is disabled`() { + initForTest({ it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.sampleRate = 1.0 }, false) @@ -973,7 +1051,7 @@ class SentryTest { @Test fun `backpressure monitor is a NoOp if handling is disabled`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.isEnableBackpressureHandling = false sentryOptions = it @@ -985,7 +1063,7 @@ class SentryTest { fun `backpressure monitor is set if handling is enabled`() { var sentryOptions: SentryOptions? = null - Sentry.init { + initForTest { it.dsn = dsn it.isEnableBackpressureHandling = true sentryOptions = it @@ -997,9 +1075,9 @@ class SentryTest { fun `init calls samplers if isEnableAppStartProfiling is true`() { val mockSampleTracer = mock() val mockProfilesSampler = mock() - Sentry.init { + initForTest { it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.isEnableAppStartProfiling = true it.profilesSampleRate = 1.0 it.tracesSampler = mockSampleTracer @@ -1028,9 +1106,9 @@ class SentryTest { fun `init calls app start profiling samplers in the background`() { val mockSampleTracer = mock() val mockProfilesSampler = mock() - Sentry.init { + initForTest { it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.isEnableAppStartProfiling = true it.profilesSampleRate = 1.0 it.tracesSampler = mockSampleTracer @@ -1047,9 +1125,9 @@ class SentryTest { fun `init does not call app start profiling samplers if cache dir is null`() { val mockSampleTracer = mock() val mockProfilesSampler = mock() - Sentry.init { + initForTest { it.dsn = dsn - it.enableTracing = true + it.tracesSampleRate = 1.0 it.isEnableAppStartProfiling = true it.profilesSampleRate = 1.0 it.tracesSampler = mockSampleTracer @@ -1063,16 +1141,14 @@ class SentryTest { } @Test - fun `init does not call app start profiling samplers if enableTracing is false`() { + fun `init does not call app start profiling samplers if performance is disabled`() { val logger = mock() - val mockTraceSampler = mock() val mockProfilesSampler = mock() - Sentry.init { + initForTest { it.dsn = dsn - it.enableTracing = false + it.tracesSampleRate = null it.isEnableAppStartProfiling = true it.profilesSampleRate = 1.0 - it.tracesSampler = mockTraceSampler it.profilesSampler = mockProfilesSampler it.executorService = ImmediateExecutorService() it.cacheDirPath = getTempPath() @@ -1080,7 +1156,6 @@ class SentryTest { it.setLogger(logger) } verify(logger).log(eq(SentryLevel.INFO), eq("Tracing is disabled and app start profiling will not start.")) - verify(mockTraceSampler, never()).sample(any()) verify(mockProfilesSampler, never()).sample(any()) } @@ -1091,7 +1166,7 @@ class SentryTest { val appStartProfilingConfigFile = File(path, "app_start_profiling_config") appStartProfilingConfigFile.createNewFile() assertTrue(appStartProfilingConfigFile.exists()) - Sentry.init { + initForTest { it.dsn = dsn it.executorService = ImmediateExecutorService() it.cacheDirPath = path @@ -1100,18 +1175,18 @@ class SentryTest { } @Test - fun `init creates app start profiling config if isEnableAppStartProfiling and enableTracing is true`() { + fun `init creates app start profiling config if isEnableAppStartProfiling and performance is enabled`() { val path = getTempPath() File(path).mkdirs() val appStartProfilingConfigFile = File(path, "app_start_profiling_config") appStartProfilingConfigFile.createNewFile() assertTrue(appStartProfilingConfigFile.exists()) - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = path it.isEnableAppStartProfiling = true it.profilesSampleRate = 1.0 - it.enableTracing = true + it.tracesSampleRate = 1.0 it.executorService = ImmediateExecutorService() } assertTrue(appStartProfilingConfigFile.exists()) @@ -1121,10 +1196,10 @@ class SentryTest { fun `init saves SentryAppStartProfilingOptions to disk`() { var options = SentryOptions() val path = getTempPath() - Sentry.init { + initForTest { it.dsn = dsn it.cacheDirPath = path - it.enableTracing = true + it.tracesSampleRate = 1.0 it.tracesSampleRate = 0.5 it.isEnableAppStartProfiling = true it.profilesSampleRate = 0.2 @@ -1142,15 +1217,65 @@ class SentryTest { } @Test - fun `metrics calls hub getMetrics`() { - val hub = mock() - Sentry.init({ + fun `init on Android throws when not using SentryAndroidOptions`() { + PlatformTestManipulator.pretendIsAndroid(true) + assertFails("You are running Android. Please, use SentryAndroid.init.") { + initForTest { + it.dsn = dsn + } + } + PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `init on Android works when using SentryAndroidOptions`() { + PlatformTestManipulator.pretendIsAndroid(true) + val options = CustomAndroidOptions().also { it.dsn = dsn - }, false) - Sentry.setCurrentHub(hub) + it.mockName() + } + initForTest(options) + options.resetName() + PlatformTestManipulator.pretendIsAndroid(false) + } - Sentry.metrics() - verify(hub).metrics() + @Test + fun `init on Java works when not using SentryAndroidOptions`() { + initForTest { + it.dsn = dsn + } + } + + @Test + fun `if globalHubMode on options is not set, uses false from init param`() { + initForTest({ o -> o.dsn = dsn }, false) + val s1 = Sentry.forkedRootScopes("s1") + val s2 = Sentry.forkedRootScopes("s2") + assertNotSame(s1, s2) + } + + @Test + fun `if globalHubMode on options is not set, uses true from init param`() { + initForTest({ o -> o.dsn = dsn }, true) + val s1 = Sentry.forkedRootScopes("s1") + val s2 = Sentry.forkedRootScopes("s2") + assertSame(s1, s2) + } + + @Test + fun `if globalHubMode on options is set, ignores false from init param`() { + initForTest({ o -> o.dsn = dsn; o.isGlobalHubMode = true }, false) + val s1 = Sentry.forkedRootScopes("s1") + val s2 = Sentry.forkedRootScopes("s2") + assertSame(s1, s2) + } + + @Test + fun `if globalHubMode on options is set, ignores true from init param`() { + initForTest({ o -> o.dsn = dsn; o.isGlobalHubMode = false }, true) + val s1 = Sentry.forkedRootScopes("s1") + val s2 = Sentry.forkedRootScopes("s2") + assertNotSame(s1, s2) } private class InMemoryOptionsObserver : IOptionsObserver { @@ -1198,11 +1323,12 @@ class SentryTest { } } - private class CustomMainThreadChecker : IMainThreadChecker { + private class CustomThreadChecker : IThreadChecker { override fun isMainThread(threadId: Long): Boolean = false override fun isMainThread(thread: Thread): Boolean = false override fun isMainThread(): Boolean = false override fun isMainThread(sentryThread: SentryThread): Boolean = false + override fun currentThreadSystemId(): Long = 0 } private class CustomMemoryCollector : @@ -1233,4 +1359,24 @@ class SentryTest { assertFalse(tempFile.exists()) return tempFile.absolutePath } + + /** + * Custom SentryOptions for Android. + * It needs to call [mockName] to change its name in io.sentry.android.core.SentryAndroidOptions. + * The name cannot be changed right away, because Sentry.init instantiates the options through reflection. + * So the name should be changed in option configuration. + * After the test, it needs to call [resetName] to reset the name back to io.sentry.SentryTest$CustomAndroidOptions, + * since it's cached internally and would break subsequent tests otherwise. + */ + private class CustomAndroidOptions : SentryOptions() { + init { + resetName() + } + fun mockName() { + javaClass.injectForField("name", "io.sentry.android.core.SentryAndroidOptions") + } + fun resetName() { + javaClass.injectForField("name", "io.sentry.SentryTest\$CustomAndroidOptions") + } + } } diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index b22f585f6dd..eb333187b29 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -3,7 +3,8 @@ package io.sentry import io.sentry.protocol.SentryId import io.sentry.protocol.TransactionNameSource import io.sentry.protocol.User -import io.sentry.util.thread.IMainThreadChecker +import io.sentry.test.createTestScopes +import io.sentry.util.thread.IThreadChecker import org.awaitility.kotlin.await import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -30,16 +31,15 @@ class SentryTracerTest { private class Fixture { val options = SentryOptions() - val hub: Hub + val scopes: Scopes val transactionPerformanceCollector: TransactionPerformanceCollector init { options.dsn = "https://key@sentry.io/proj" options.environment = "environment" options.release = "release@3.0.0" - hub = spy(Hub(options)) + scopes = spy(createTestScopes(options)) transactionPerformanceCollector = spy(DefaultTransactionPerformanceCollector(options)) - hub.bindClient(mock()) } fun getSut( @@ -62,12 +62,38 @@ class SentryTracerTest { transactionOptions.deadlineTimeout = deadlineTimeout transactionOptions.isTrimEnd = trimEnd transactionOptions.transactionFinishedCallback = transactionFinishedCallback - return SentryTracer(TransactionContext("name", "op", samplingDecision), hub, transactionOptions, performanceCollector) + return SentryTracer(TransactionContext("name", "op", samplingDecision), scopes, transactionOptions, performanceCollector) } } private val fixture = Fixture() + @Test + fun `transfer origin from transaction options to transaction context`() { + fixture.getSut() + val transactionOptions = TransactionOptions().also { + it.origin = "new-origin" + } + val transactionContext = TransactionContext("name", "op", null).also { + it.origin = "old-origin" + } + + val transaction = SentryTracer(transactionContext, fixture.scopes, transactionOptions, null) + assertEquals("new-origin", transaction.spanContext.origin) + } + + @Test + fun `does not create child span if origin is ignored`() { + val tracer = fixture.getSut({ + it.setDebug(true) + it.setLogger(SystemOutLogger()) + it.setIgnoredSpanOrigins(listOf("ignored")) + }) + tracer.startChild("child1", null, SpanOptions().also { it.origin = "ignored" }) + tracer.startChild("child2") + assertEquals(1, tracer.children.size) + } + @Test fun `does not add more spans than configured in options`() { val tracer = fixture.getSut({ @@ -151,7 +177,7 @@ class SentryTracerTest { fun `when transaction is finished, transaction is captured`() { val tracer = fixture.getSut() tracer.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(it.transaction, tracer.name) }, @@ -186,10 +212,10 @@ class SentryTracerTest { @Test fun `when transaction is finished, transaction is cleared from the scope`() { val tracer = fixture.getSut() - fixture.hub.configureScope { it.transaction = tracer } - assertNotNull(fixture.hub.span) + fixture.scopes.configureScope { it.transaction = tracer } + assertNotNull(fixture.scopes.span) tracer.finish() - assertNull(fixture.hub.span) + assertNull(fixture.scopes.span) } @Test @@ -198,7 +224,7 @@ class SentryTracerTest { val ex = RuntimeException() tracer.throwable = ex tracer.finish() - verify(fixture.hub).setSpanContext(ex, tracer.root, "name") + verify(fixture.scopes).setSpanContext(ex, tracer.root, "name") } @Test @@ -207,7 +233,7 @@ class SentryTracerTest { tracer.setTag("tag1", "val1") tracer.setTag("tag2", "val2") tracer.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(mapOf("tag1" to "val1", "tag2" to "val2"), it.tags) assertNotNull(it.contexts.trace) { @@ -227,7 +253,7 @@ class SentryTracerTest { val span = tracer.startChild("op2") span.spanContext.sampled = false tracer.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) assertEquals("op1", it.spans.first().op) @@ -254,7 +280,7 @@ class SentryTracerTest { tracer.setContext("otel", otelContext) tracer.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(otelContext, it.contexts["otel"]) }, @@ -405,8 +431,8 @@ class SentryTracerTest { transaction.finish(SpanStatus.UNKNOWN_ERROR) // call only once - verify(fixture.hub).setSpanContext(ex, transaction.root, "name") - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).setSpanContext(ex, transaction.root, "name") + verify(fixture.scopes).captureTransaction( check { assertNotNull(it.contexts.trace) { assertEquals(SpanStatus.OK, it.status) @@ -487,20 +513,20 @@ class SentryTracerTest { } @Test - fun `when waiting for children, finishing transaction does not call hub if all children are not finished`() { + fun `when waiting for children, finishing transaction does not call scopes if all children are not finished`() { val transaction = fixture.getSut(waitForChildren = true) transaction.startChild("op") transaction.finish() - verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) + verify(fixture.scopes, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) } @Test - fun `when waiting for children, finishing transaction calls hub if all children are finished`() { + fun `when waiting for children, finishing transaction calls scopes if all children are finished`() { val transaction = fixture.getSut(waitForChildren = true) val child = transaction.startChild("op") child.finish() transaction.finish() - verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.scopes).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -517,21 +543,21 @@ class SentryTracerTest { } @Test - fun `when waiting for children, hub is not called until transaction is finished`() { + fun `when waiting for children, scopes is not called until transaction is finished`() { val transaction = fixture.getSut(waitForChildren = true) val child = transaction.startChild("op") child.finish() - verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) + verify(fixture.scopes, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) } @Test - fun `when waiting for children, finishing last child calls hub if transaction is already finished`() { + fun `when waiting for children, finishing last child calls scopes if transaction is already finished`() { val transaction = fixture.getSut(waitForChildren = true) val child = transaction.startChild("op") transaction.finish(SpanStatus.INVALID_ARGUMENT) - verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) + verify(fixture.scopes, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) child.finish() - verify(fixture.hub, times(1)).captureTransaction( + verify(fixture.scopes, times(1)).captureTransaction( check { assertEquals(SpanStatus.INVALID_ARGUMENT, it.status) }, @@ -553,7 +579,7 @@ class SentryTracerTest { transaction.finish(SpanStatus.INVALID_ARGUMENT) - verify(fixture.hub, times(1)).captureTransaction( + verify(fixture.scopes, times(1)).captureTransaction( check { assertEquals(2, it.spans.size) // span status/timestamp is retained @@ -576,14 +602,13 @@ class SentryTracerTest { it.isTraceSampling = true it.isSendDefaultPii = true }) - fixture.hub.setUser( + fixture.scopes.setUser( User().apply { id = "user-id" - others = mapOf("segment" to "pro") } ) val replayId = SentryId() - fixture.hub.configureScope { it.replayId = replayId } + fixture.scopes.configureScope { it.replayId = replayId } val trace = transaction.traceContext() assertNotNull(trace) { assertEquals(transaction.spanContext.traceId, it.traceId) @@ -600,10 +625,9 @@ class SentryTracerTest { val transaction = fixture.getSut({ it.isTraceSampling = true }) - fixture.hub.setUser( + fixture.scopes.setUser( User().apply { id = "user-id" - others = mapOf("segment" to "pro") } ) val trace = transaction.traceContext() @@ -614,7 +638,6 @@ class SentryTracerTest { assertEquals("release@3.0.0", it.release) assertEquals(transaction.name, it.transaction) assertNull(it.userId) - assertEquals("pro", it.userSegment) } } @@ -624,7 +647,7 @@ class SentryTracerTest { it.isTraceSampling = true }) val traceBeforeUserSet = transaction.traceContext() - fixture.hub.setUser( + fixture.scopes.setUser( User().apply { id = "user-id" } @@ -638,10 +661,8 @@ class SentryTracerTest { assertEquals(it.publicKey, traceBeforeUserSet?.publicKey) assertEquals(it.sampleRate, traceBeforeUserSet?.sampleRate) assertEquals(it.userId, traceBeforeUserSet?.userId) - assertEquals(it.userSegment, traceBeforeUserSet?.userSegment) assertNull(it.userId) - assertNull(it.userSegment) } } @@ -654,14 +675,13 @@ class SentryTracerTest { it.isSendDefaultPii = true }) - fixture.hub.setUser( + fixture.scopes.setUser( User().apply { id = "userId12345" - others = mapOf("segment" to "pro") } ) val replayId = SentryId() - fixture.hub.configureScope { it.replayId = replayId } + fixture.scopes.configureScope { it.replayId = replayId } val header = transaction.toBaggageHeader(null) assertNotNull(header) { @@ -672,9 +692,8 @@ class SentryTracerTest { assertTrue(it.value.contains("sentry-public_key=key,")) assertTrue(it.value.contains("sentry-release=1.0.99-rc.7,")) assertTrue(it.value.contains("sentry-environment=production,")) - assertTrue(it.value.contains("sentry-transaction=name,")) + assertTrue(it.value.contains("sentry-transaction=name")) // assertTrue(it.value.contains("sentry-user_id=userId12345,")) - assertTrue(it.value.contains("sentry-user_segment=pro$".toRegex())) assertTrue(it.value.contains("sentry-replay_id=$replayId")) } } @@ -687,10 +706,9 @@ class SentryTracerTest { it.release = "1.0.99-rc.7" }) - fixture.hub.setUser( + fixture.scopes.setUser( User().apply { id = "userId12345" - others = mapOf("segment" to "pro") } ) @@ -703,9 +721,8 @@ class SentryTracerTest { assertTrue(it.value.contains("sentry-public_key=key,")) assertTrue(it.value.contains("sentry-release=1.0.99-rc.7,")) assertTrue(it.value.contains("sentry-environment=production,")) - assertTrue(it.value.contains("sentry-transaction=name,")) + assertTrue(it.value.contains("sentry-transaction=name")) assertFalse(it.value.contains("sentry-user_id")) - assertTrue(it.value.contains("sentry-user_segment=pro$".toRegex())) } } @@ -718,7 +735,7 @@ class SentryTracerTest { it.isSendDefaultPii = true }) - fixture.hub.setUser(null) + fixture.scopes.setUser(null) val header = transaction.toBaggageHeader(null) assertNotNull(header) { @@ -731,18 +748,17 @@ class SentryTracerTest { assertTrue(it.value.contains("sentry-environment=production,")) assertTrue(it.value.contains("sentry-transaction=name")) assertFalse(it.value.contains("sentry-user_id")) - assertFalse(it.value.contains("sentry-user_segment")) } } @Test - fun `sets ITransaction data as extra in SentryTransaction`() { + fun `sets ITransaction data as tracecontext data in SentryTransaction`() { val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) transaction.setData("key", "val") transaction.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { - assertEquals("val", it.getExtra("key")) + assertEquals("val", it.contexts.trace?.data?.get("key")) }, anyOrNull(), anyOrNull(), @@ -757,7 +773,7 @@ class SentryTracerTest { span.setData("key", "val") span.finish() transaction.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertNotNull(it.spans.first().data) { assertEquals("val", it["key"]) @@ -845,7 +861,7 @@ class SentryTracerTest { await.untilFalse(transaction.isFinishTimerRunning) - verify(fixture.hub, never()).captureTransaction( + verify(fixture.scopes, never()).captureTransaction( anyOrNull(), anyOrNull(), anyOrNull(), @@ -862,7 +878,7 @@ class SentryTracerTest { await.untilFalse(transaction.isFinishTimerRunning) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( anyOrNull(), anyOrNull(), anyOrNull(), @@ -921,7 +937,7 @@ class SentryTracerTest { await.untilFalse(transaction.isFinishTimerRunning) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(2, it.spans.size) assertEquals(transaction.root.finishDate, span2.finishDate) @@ -959,7 +975,7 @@ class SentryTracerTest { transaction.setMeasurement("days", 2, MeasurementUnit.Duration.DAY) transaction.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1.0f, it.measurements["metric1"]!!.value) assertEquals(null, it.measurements["metric1"]!!.unit) @@ -980,7 +996,7 @@ class SentryTracerTest { transaction.setMeasurement("metric1", 2, MeasurementUnit.Duration.DAY) transaction.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(2, it.measurements["metric1"]!!.value) assertEquals("day", it.measurements["metric1"]!!.unit) @@ -998,7 +1014,7 @@ class SentryTracerTest { transaction.setMeasurementFromChild("metric1", 2, MeasurementUnit.Duration.DAY) transaction.finish() - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1.0f, it.measurements["metric1"]!!.value) assertNull(it.measurements["metric1"]!!.unit) @@ -1068,7 +1084,7 @@ class SentryTracerTest { assertTrue(span.isFinished) // and the transaction should be captured - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) assertEquals(transaction.root.finishDate!!.nanoTimestamp(), span.finishDate!!.nanoTimestamp()) @@ -1098,7 +1114,7 @@ class SentryTracerTest { assertTrue(span.isFinished) // and the transaction should be captured - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(1, it.spans.size) assertEquals(transactionFinishDate, span.finishDate) @@ -1139,7 +1155,7 @@ class SentryTracerTest { assertEquals(expectedParentStartDate, parentSpan.startDate) assertEquals(expectedParentEndDate, parentSpan.finishDate) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(3, it.spans.size) }, @@ -1179,7 +1195,7 @@ class SentryTracerTest { assertEquals(expectedParentStartDate, parentSpan.startDate) assertEquals(expectedParentEndDate, parentSpan.finishDate) - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(3, it.spans.size) }, @@ -1270,7 +1286,7 @@ class SentryTracerTest { assertEquals(transaction.finishDate, span1.finishDate) // and the transaction should be captured with both spans - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(2, it.spans.size) }, @@ -1293,7 +1309,7 @@ class SentryTracerTest { transaction.forceFinish(SpanStatus.ABORTED, false, null) // then a transaction should be captured with 0 spans - verify(fixture.hub).captureTransaction( + verify(fixture.scopes).captureTransaction( check { assertEquals(0, it.spans.size) }, @@ -1316,7 +1332,7 @@ class SentryTracerTest { transaction.forceFinish(SpanStatus.ABORTED, true, null) // then the transaction should be captured with 0 spans - verify(fixture.hub, never()).captureTransaction( + verify(fixture.scopes, never()).captureTransaction( anyOrNull(), anyOrNull(), anyOrNull(), @@ -1340,16 +1356,16 @@ class SentryTracerTest { tracer.scheduleFinish() assertTrue(tracer.isFinished) - verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + verify(fixture.scopes).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test fun `when a span is launched on the main thread, the thread info should be set correctly`() { - val mainThreadChecker = mock() - whenever(mainThreadChecker.isMainThread).thenReturn(true) + val threadChecker = mock() + whenever(threadChecker.isMainThread).thenReturn(true) val tracer = fixture.getSut(optionsConfiguration = { options -> - options.mainThreadChecker = mainThreadChecker + options.threadChecker = threadChecker }) val span = tracer.startChild("span.op") assertNotNull(span.getData(SpanDataConvention.THREAD_ID)) @@ -1358,11 +1374,11 @@ class SentryTracerTest { @Test fun `when a span is launched on the background thread, the thread info should be set correctly`() { - val mainThreadChecker = mock() - whenever(mainThreadChecker.isMainThread).thenReturn(false) + val threadChecker = mock() + whenever(threadChecker.isMainThread).thenReturn(false) val tracer = fixture.getSut(optionsConfiguration = { options -> - options.mainThreadChecker = mainThreadChecker + options.threadChecker = threadChecker }) val span = tracer.startChild("span.op") assertNotNull(span.getData(SpanDataConvention.THREAD_ID)) diff --git a/sentry/src/test/java/io/sentry/SentryUUIDTest.kt b/sentry/src/test/java/io/sentry/SentryUUIDTest.kt new file mode 100644 index 00000000000..cdb8b92684a --- /dev/null +++ b/sentry/src/test/java/io/sentry/SentryUUIDTest.kt @@ -0,0 +1,19 @@ +package io.sentry + +import junit.framework.TestCase.assertEquals +import kotlin.test.Test + +class SentryUUIDTest { + + @Test + fun `generated SentryID is 32 characters long`() { + val sentryId = SentryUUID.generateSentryId() + assertEquals(32, sentryId.length) + } + + @Test + fun `generated SpanID is 16 characters long`() { + val sentryId = SentryUUID.generateSpanId() + assertEquals(16, sentryId.length) + } +} diff --git a/sentry/src/test/java/io/sentry/SentryWrapperTest.kt b/sentry/src/test/java/io/sentry/SentryWrapperTest.kt index 7f6d449eac5..36660ef026c 100644 --- a/sentry/src/test/java/io/sentry/SentryWrapperTest.kt +++ b/sentry/src/test/java/io/sentry/SentryWrapperTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.test.initForTest import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -27,28 +28,28 @@ class SentryWrapperTest { } @Test - fun `hub is reset to its state within the thread after supply is done`() { - Sentry.init { + fun `scopes is reset to state within the thread after isolated supply is done`() { + initForTest { it.dsn = dsn it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> event } } - val mainHub = Sentry.getCurrentHub() - val threadedHub = Sentry.getCurrentHub().clone() + val mainScopes = Sentry.getCurrentScopes() + val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test") executor.submit { - Sentry.setCurrentHub(threadedHub) + Sentry.setCurrentScopes(threadedScopes) }.get() - assertEquals(mainHub, Sentry.getCurrentHub()) + assertEquals(mainScopes, Sentry.getCurrentScopes()) val callableFuture = CompletableFuture.supplyAsync( SentryWrapper.wrapSupplier { - assertNotEquals(mainHub, Sentry.getCurrentHub()) - assertNotEquals(threadedHub, Sentry.getCurrentHub()) + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) "Result 1" }, executor @@ -57,16 +58,16 @@ class SentryWrapperTest { callableFuture.join() executor.submit { - assertNotEquals(mainHub, Sentry.getCurrentHub()) - assertEquals(threadedHub, Sentry.getCurrentHub()) + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertEquals(threadedScopes, Sentry.getCurrentScopes()) }.get() } @Test - fun `wrapped supply async isolates Hubs`() { + fun `wrapped supply async isolates Scopes`() { val capturedEvents = mutableListOf() - Sentry.init { + initForTest { it.dsn = dsn it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> capturedEvents.add(event) @@ -115,10 +116,10 @@ class SentryWrapperTest { } @Test - fun `wrapped callable isolates Hubs`() { + fun `wrapped callable isolates Scopes`() { val capturedEvents = mutableListOf() - Sentry.init { + initForTest { it.dsn = dsn it.beforeSend = SentryOptions.BeforeSendCallback { event, hint -> capturedEvents.add(event) @@ -164,25 +165,25 @@ class SentryWrapperTest { } @Test - fun `hub is reset to its state within the thread after callable is done`() { - Sentry.init { + fun `scopes is reset to state within the thread after isolated callable is done`() { + initForTest { it.dsn = dsn } - val mainHub = Sentry.getCurrentHub() - val threadedHub = Sentry.getCurrentHub().clone() + val mainScopes = Sentry.getCurrentScopes() + val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test") executor.submit { - Sentry.setCurrentHub(threadedHub) + Sentry.setCurrentScopes(threadedScopes) }.get() - assertEquals(mainHub, Sentry.getCurrentHub()) + assertEquals(mainScopes, Sentry.getCurrentScopes()) val callableFuture = executor.submit( SentryWrapper.wrapCallable { - assertNotEquals(mainHub, Sentry.getCurrentHub()) - assertNotEquals(threadedHub, Sentry.getCurrentHub()) + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) "Result 1" } ) @@ -190,8 +191,8 @@ class SentryWrapperTest { callableFuture.get() executor.submit { - assertNotEquals(mainHub, Sentry.getCurrentHub()) - assertEquals(threadedHub, Sentry.getCurrentHub()) + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertEquals(threadedScopes, Sentry.getCurrentScopes()) }.get() } } diff --git a/sentry/src/test/java/io/sentry/SessionAdapterTest.kt b/sentry/src/test/java/io/sentry/SessionAdapterTest.kt index 0709d77828c..0f29942e360 100644 --- a/sentry/src/test/java/io/sentry/SessionAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/SessionAdapterTest.kt @@ -5,7 +5,6 @@ import org.mockito.kotlin.mock import java.io.StringReader import java.io.StringWriter import java.lang.Exception -import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -23,7 +22,7 @@ class SessionAdapterTest { null, 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -47,7 +46,7 @@ class SessionAdapterTest { null, 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -71,7 +70,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, null, - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -95,7 +94,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", null, 123456.toLong(), 6000.toDouble(), @@ -119,7 +118,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, null, 6000.toDouble(), @@ -143,7 +142,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), null, @@ -167,7 +166,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -191,7 +190,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -215,7 +214,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -239,7 +238,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -263,7 +262,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -287,7 +286,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -330,7 +329,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -373,7 +372,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -512,7 +511,7 @@ class SessionAdapterTest { null, 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), @@ -571,7 +570,7 @@ class SessionAdapterTest { @Test fun `missing abnormal_mechanism does not serialize `() { val json = "{\n" + - " \"sid\": \"not a uuid\",\n" + + " \"sid\": \"c81d4e2e-bcf2-11e6-869b-7df92533d2db\",\n" + " \"did\": \"123\",\n" + " \"init\": true,\n" + " \"status\": \"ok\",\n" + @@ -595,7 +594,7 @@ class SessionAdapterTest { DateUtils.getDateTime("2020-02-07T14:16:00.001Z"), 2, "123", - null, + "c81d4e2e-bcf2-11e6-869b-7df92533d2db", true, 123456.toLong(), 6000.toDouble(), diff --git a/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt b/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt index 8218740b89d..94250685a81 100644 --- a/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/ShutdownHookIntegrationTest.kt @@ -16,7 +16,7 @@ class ShutdownHookIntegrationTest { private class Fixture { val runtime = mock() val options = SentryOptions() - val hub = mock() + val scopes = mock() fun getSut(): ShutdownHookIntegration { return ShutdownHookIntegration(runtime) @@ -29,7 +29,7 @@ class ShutdownHookIntegrationTest { fun `registration attaches shutdown hook to runtime`() { val integration = fixture.getSut() - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.runtime).addShutdownHook(any()) } @@ -39,7 +39,7 @@ class ShutdownHookIntegrationTest { val integration = fixture.getSut() fixture.options.isEnableShutdownHook = false - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.runtime, never()).addShutdownHook(any()) } @@ -48,7 +48,7 @@ class ShutdownHookIntegrationTest { fun `registration removes shutdown hook from runtime`() { val integration = fixture.getSut() - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) integration.close() verify(fixture.runtime).removeShutdownHook(any()) @@ -58,13 +58,13 @@ class ShutdownHookIntegrationTest { fun `hook calls flush`() { val integration = fixture.getSut() - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) assertNotNull(integration.hook) { it.start() it.join() } - verify(fixture.hub).flush(any()) + verify(fixture.scopes).flush(any()) } @Test @@ -72,13 +72,13 @@ class ShutdownHookIntegrationTest { val integration = fixture.getSut() fixture.options.flushTimeoutMillis = 10000 - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) assertNotNull(integration.hook) { it.start() it.join() } - verify(fixture.hub).flush(eq(10000)) + verify(fixture.scopes).flush(eq(10000)) } @Test @@ -86,7 +86,7 @@ class ShutdownHookIntegrationTest { val integration = fixture.getSut() whenever(fixture.runtime.removeShutdownHook(any())).thenThrow(java.lang.IllegalStateException("Shutdown in progress")) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) integration.close() verify(fixture.runtime).removeShutdownHook(any()) @@ -97,7 +97,7 @@ class ShutdownHookIntegrationTest { val integration = fixture.getSut() whenever(fixture.runtime.addShutdownHook(any())).thenThrow(java.lang.IllegalStateException("VM already shutting down")) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) verify(fixture.runtime).addShutdownHook(any()) } @@ -107,7 +107,7 @@ class ShutdownHookIntegrationTest { val integration = fixture.getSut() whenever(fixture.runtime.removeShutdownHook(any())).thenThrow(java.lang.IllegalStateException()) - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) assertFails { integration.close() @@ -120,7 +120,7 @@ class ShutdownHookIntegrationTest { fun `Integration adds itself to integration list`() { val integration = fixture.getSut() - integration.register(fixture.hub, fixture.options) + integration.register(fixture.scopes, fixture.options) assertTrue( fixture.options.sdkVersion!!.integrationSet.contains("ShutdownHook") diff --git a/sentry/src/test/java/io/sentry/SpanStatusTest.kt b/sentry/src/test/java/io/sentry/SpanStatusTest.kt index 37cf76882aa..4b00d8fddc2 100644 --- a/sentry/src/test/java/io/sentry/SpanStatusTest.kt +++ b/sentry/src/test/java/io/sentry/SpanStatusTest.kt @@ -21,14 +21,19 @@ class SpanStatusTest { assertEquals(SpanStatus.INTERNAL_ERROR, SpanStatus.fromHttpStatusCode(500)) } + @Test + fun `code 3xx is now also considered OK`() { + assertEquals(SpanStatus.OK, SpanStatus.fromHttpStatusCode(304)) + } + @Test fun `returns null when no SpanStatus matches specific code`() { - assertNull(SpanStatus.fromHttpStatusCode(302)) + assertNull(SpanStatus.fromHttpStatusCode(599)) } @Test fun `returns default value when no SpanStatus matches specific code`() { - assertEquals(SpanStatus.UNKNOWN_ERROR, SpanStatus.fromHttpStatusCode(302, SpanStatus.UNKNOWN_ERROR)) + assertEquals(SpanStatus.UNKNOWN_ERROR, SpanStatus.fromHttpStatusCode(599, SpanStatus.UNKNOWN_ERROR)) } @Test diff --git a/sentry/src/test/java/io/sentry/SpanTest.kt b/sentry/src/test/java/io/sentry/SpanTest.kt index 09bf01c791d..79c374413c0 100644 --- a/sentry/src/test/java/io/sentry/SpanTest.kt +++ b/sentry/src/test/java/io/sentry/SpanTest.kt @@ -21,10 +21,10 @@ import kotlin.test.assertTrue class SpanTest { private class Fixture { - val hub = mock() + val scopes = mock() init { - whenever(hub.options).thenReturn( + whenever(scopes.options).thenReturn( SentryOptions().apply { dsn = "https://key@sentry.io/proj" isTraceSampling = true @@ -33,20 +33,27 @@ class SpanTest { } fun getSut(options: SpanOptions = SpanOptions()): Span { - return Span( + val context = SpanContext( SentryId(), SpanId(), - SentryTracer(TransactionContext("name", "op"), hub), + SpanId(), "op", - hub, null, + null, + null, + null + ) + return Span( + SentryTracer(TransactionContext("name", "op"), scopes), + scopes, + context, options, null ) } fun getRootSut(options: TransactionOptions = TransactionOptions()): Span { - return SentryTracer(TransactionContext("name", "op"), hub, options).root + return SentryTracer(TransactionContext("name", "op"), scopes, options).root } } @@ -101,15 +108,25 @@ class SpanTest { fun `converts to Sentry trace header`() { val traceId = SentryId() val parentSpanId = SpanId() - val span = Span( + val spanContext = SpanContext( traceId, + SpanId(), parentSpanId, + "op", + null, + TracesSamplingDecision(true), + null, + null + ) + val span = Span( SentryTracer( TransactionContext("name", "op", TracesSamplingDecision(true)), - fixture.hub + fixture.scopes ), - "op", - fixture.hub + fixture.scopes, + spanContext, + SpanOptions(), + null ) val sentryTrace = span.toSentryTrace() @@ -120,6 +137,38 @@ class SpanTest { } } + @Test + fun `transfers span origin from options to span context`() { + val traceId = SentryId() + val parentSpanId = SpanId() + val spanContext = SpanContext( + traceId, + SpanId(), + parentSpanId, + "op", + null, + TracesSamplingDecision(true), + null, + "old-origin" + ) + + val spanOptions = SpanOptions() + spanOptions.origin = "new-origin" + + val span = Span( + SentryTracer( + TransactionContext("name", "op", TracesSamplingDecision(true)), + fixture.scopes + ), + fixture.scopes, + spanContext, + spanOptions, + null + ) + + assertEquals("new-origin", span.spanContext.origin) + } + @Test fun `starting a child with details adds span to transaction`() { val transaction = getTransaction() @@ -163,17 +212,17 @@ class SpanTest { } @Test - fun `when span has throwable set set, it assigns itself to throwable on the Hub`() { + fun `when span has throwable set set, it assigns itself to throwable on the Scopes`() { val transaction = SentryTracer( TransactionContext("name", "op"), - fixture.hub + fixture.scopes ) val span = transaction.startChild("op") val ex = RuntimeException() span.throwable = ex span.finish() - verify(fixture.hub).setSpanContext(ex, span, "name") + verify(fixture.scopes).setSpanContext(ex, span, "name") } @Test @@ -188,7 +237,7 @@ class SpanTest { span.finish(SpanStatus.UNKNOWN_ERROR) // call only once - verify(fixture.hub).setSpanContext(any(), any(), any()) + verify(fixture.scopes).setSpanContext(any(), any(), any()) assertEquals(SpanStatus.OK, span.status) assertEquals(timestamp, span.finishDate) } @@ -410,7 +459,6 @@ class SpanTest { assertEquals(transactionTraceContext.publicKey, spanTraceContext.publicKey) assertEquals(transactionTraceContext.sampleRate, spanTraceContext.sampleRate) assertEquals(transactionTraceContext.userId, spanTraceContext.userId) - assertEquals(transactionTraceContext.userSegment, spanTraceContext.userSegment) } @Test @@ -486,15 +534,6 @@ class SpanTest { assertEquals(1, transaction.root.measurements["test"]!!.value) } - @Test - fun `span provides local metrics aggregator instance`() { - val span = fixture.getSut() - assertNotNull(span.localMetricsAggregator) - - // ensure the getter returns the same instance - assertSame(span.localMetricsAggregator, span.localMetricsAggregator) - } - // test to ensure that the span is not finished when the finishCallback is called @Test fun `span is not finished when finishCallback is called`() { @@ -509,7 +548,7 @@ class SpanTest { } private fun getTransaction(transactionContext: TransactionContext = TransactionContext("name", "op")): SentryTracer { - return SentryTracer(transactionContext, fixture.hub) + return SentryTracer(transactionContext, fixture.scopes) } private fun startChildFromSpan(): Span { diff --git a/sentry/src/test/java/io/sentry/StackTest.kt b/sentry/src/test/java/io/sentry/StackTest.kt index 13089ab6a48..c8b0aa8a9f7 100644 --- a/sentry/src/test/java/io/sentry/StackTest.kt +++ b/sentry/src/test/java/io/sentry/StackTest.kt @@ -1,6 +1,7 @@ package io.sentry import io.sentry.Stack.StackItem +import io.sentry.test.createSentryClientMock import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals @@ -10,7 +11,7 @@ class StackTest { private class Fixture { val options = SentryOptions() - val client = mock() + val client = createSentryClientMock() val scope = Scope(options) lateinit var rootItem: StackItem diff --git a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt index 876ec128315..8b00df543d6 100644 --- a/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/TraceContextSerializationTest.kt @@ -1,7 +1,7 @@ package io.sentry import io.sentry.protocol.SentryId -import io.sentry.protocol.User +import io.sentry.protocol.TransactionNameSource import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -21,7 +21,6 @@ class TraceContextSerializationTest { "9ee2c92c-401e-4296-b6f0-fb3b13edd9ee", "0666ab02-6364-4135-aa59-02e8128ce052", "c052c566-6619-45f5-a61f-172802afa39a", - "f7d8662b-5551-4ef8-b6a8-090f0561a530", "0252ec25-cd0a-4230-bd2f-936a4585637e", "0.00000021", "true", @@ -55,14 +54,10 @@ class TraceContextSerializationTest { private fun createTraceContext(sRate: Double): TraceContext { val baggage = Baggage(fixture.logger) - val hub: IHub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes: IScopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) baggage.setValuesFromTransaction( - SentryTracer(TransactionContext("name", "op"), hub), - User().apply { - id = "user-id" - others = mapOf("segment" to "pro") - }, + SentryId(), SentryId(), SentryOptions().apply { dsn = dsnString @@ -70,20 +65,13 @@ class TraceContextSerializationTest { release = "1.0.17" tracesSampleRate = sRate }, - TracesSamplingDecision(sRate > 0.5, sRate) + TracesSamplingDecision(sRate > 0.5, sRate), + "name", + TransactionNameSource.ROUTE ) return baggage.toTraceContext()!! } - @Test - fun `can still parse legacy JSON with non flat user`() { - val expectedJson = sanitizedFile("json/trace_state_no_sample_rate.json") - val legacyJson = sanitizedFile("json/trace_state_legacy.json") - val actual = deserialize(legacyJson) - val actualJson = serialize(actual) - assertEquals(expectedJson, actualJson) - } - // Helper private fun sanitizedFile(path: String): String { diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index 52e294c54dc..06eb60aece2 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -16,7 +16,6 @@ class TracesSamplerTest { class Fixture { internal fun getSut( randomResult: Double? = null, - enableTracing: Boolean? = null, tracesSampleRate: Double? = null, profilesSampleRate: Double? = null, tracesSamplerCallback: SentryOptions.TracesSamplerCallback? = null, @@ -28,9 +27,6 @@ class TracesSamplerTest { whenever(random.nextDouble()).thenReturn(randomResult) } val options = SentryOptions() - if (enableTracing != null) { - options.enableTracing = enableTracing - } if (tracesSampleRate != null) { options.tracesSampleRate = tracesSampleRate } @@ -53,14 +49,6 @@ class TracesSamplerTest { private val fixture = Fixture() - @Test - fun `when no tracesSampleRate is set, uses default rate`() { - val sampler = fixture.getSut(randomResult = 0.9, enableTracing = true) - val samplingDecision = sampler.sample(SamplingContext(TransactionContext("name", "op"), null)) - assertTrue(samplingDecision.sampled) - assertEquals(1.0, samplingDecision.sampleRate) - } - @Test fun `when tracesSampleRate is set and random returns greater number returns false`() { val sampler = fixture.getSut(randomResult = 0.9, tracesSampleRate = 0.2, profilesSampleRate = 0.2) diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index b1f324f06fc..d6b715bd841 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -21,16 +21,6 @@ class TransactionContextTest { assertFalse(context.isForNextAppStart) } - @Test - fun `when context is created from trace header, parent sampling decision is set`() { - val header = SentryTraceHeader(SentryId(), SpanId(), true) - val context = TransactionContext.fromSentryTrace("name", "op", header) - assertNull(context.sampled) - assertNull(context.profileSampled) - assertTrue(context.parentSampled!!) - assertFalse(context.isForNextAppStart) - } - @Test fun `when context is created from propagation context, parent sampling decision of false is set from trace header`() { val logger = mock() diff --git a/sentry/src/test/java/io/sentry/UUIDStringUtilsTest.kt b/sentry/src/test/java/io/sentry/UUIDStringUtilsTest.kt new file mode 100644 index 00000000000..c6a1901d93d --- /dev/null +++ b/sentry/src/test/java/io/sentry/UUIDStringUtilsTest.kt @@ -0,0 +1,23 @@ +package io.sentry + +import io.sentry.util.UUIDStringUtils +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals + +class UUIDStringUtilsTest { + + @Test + fun `UUID toString matches UUIDStringUtils to String`() { + val uuid = UUID.randomUUID() + val sentryIdString = uuid.toString().replace("-", "") + assertEquals(sentryIdString, UUIDStringUtils.toSentryIdString(uuid)) + } + + @Test + fun `UUID toString matches UUIDStringUtils to String for SpanId`() { + val uuid = UUID.randomUUID() + val sentryIdString = uuid.toString().replace("-", "").substring(0, 16) + assertEquals(sentryIdString, UUIDStringUtils.toSentrySpanIdString(uuid)) + } +} diff --git a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt index aaa8cbe3fc6..409d3b971b1 100644 --- a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt @@ -5,6 +5,7 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.hints.DiskFlushNotification import io.sentry.hints.EventDropReason.MULTITHREADED_DEDUPLICATION import io.sentry.protocol.SentryId +import io.sentry.test.createTestScopes import io.sentry.util.HintUtils import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -32,7 +33,7 @@ class UncaughtExceptionHandlerIntegrationTest { val defaultHandler = mock() val thread = mock() val throwable = Throwable("test") - val hub = mock() + val scopes = mock() val options = SentryOptions() val logger = mock() @@ -65,17 +66,17 @@ class UncaughtExceptionHandlerIntegrationTest { fun `when uncaughtException is called, sentry captures exception`() { val sut = fixture.getSut(isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test fun `when register is called, current handler is not lost`() { val sut = fixture.getSut(hasDefaultHandler = true, isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) verify(fixture.defaultHandler).uncaughtException(fixture.thread, fixture.throwable) @@ -83,7 +84,7 @@ class UncaughtExceptionHandlerIntegrationTest { @Test fun `when uncaughtException is called, exception captured has handled=false`() { - whenever(fixture.hub.captureException(any())).thenAnswer { invocation -> + whenever(fixture.scopes.captureException(any())).thenAnswer { invocation -> val e = invocation.getArgument(1) assertNotNull(e) assertNotNull(e.exceptionMechanism) @@ -93,22 +94,22 @@ class UncaughtExceptionHandlerIntegrationTest { val sut = fixture.getSut(isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) } @Test - fun `when hub is closed, integrations should be closed`() { + fun `when scopes is closed, integrations should be closed`() { val integrationMock = mock() val options = SentryOptions() options.dsn = "https://key@sentry.io/proj" options.addIntegration(integrationMock) options.cacheDirPath = fixture.file.absolutePath options.setSerializer(mock()) - val hub = Hub(options) - hub.close() + val scopes = createTestScopes(options) + scopes.close() verify(integrationMock).close() } @@ -119,7 +120,7 @@ class UncaughtExceptionHandlerIntegrationTest { isPrintUncaughtStackTrace = false ) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.handler, never()).defaultUncaughtExceptionHandler = any() } @@ -128,7 +129,7 @@ class UncaughtExceptionHandlerIntegrationTest { fun `When defaultUncaughtExceptionHandler is enabled, should install Sentry UncaughtExceptionHandler`() { val sut = fixture.getSut(isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) verify(fixture.handler).defaultUncaughtExceptionHandler = argWhere { it is UncaughtExceptionHandlerIntegration } @@ -138,7 +139,7 @@ class UncaughtExceptionHandlerIntegrationTest { fun `When defaultUncaughtExceptionHandler is set and integration is closed, default uncaught exception handler is reset to previous handler`() { val sut = fixture.getSut(hasDefaultHandler = true, isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) whenever(fixture.handler.defaultUncaughtExceptionHandler) .thenReturn(sut) sut.close() @@ -150,7 +151,7 @@ class UncaughtExceptionHandlerIntegrationTest { fun `When defaultUncaughtExceptionHandler is not set and integration is closed, default uncaught exception handler is reset to null`() { val sut = fixture.getSut(isPrintUncaughtStackTrace = false) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) whenever(fixture.handler.defaultUncaughtExceptionHandler) .thenReturn(sut) sut.close() @@ -167,7 +168,7 @@ class UncaughtExceptionHandlerIntegrationTest { val sut = fixture.getSut(isPrintUncaughtStackTrace = true) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, RuntimeException("This should be printed!")) assertTrue( @@ -187,7 +188,7 @@ class UncaughtExceptionHandlerIntegrationTest { fun `waits for event to flush on disk`() { val capturedEventId = SentryId() - whenever(fixture.hub.captureEvent(any(), any())).thenAnswer { invocation -> + whenever(fixture.scopes.captureEvent(any(), any())).thenAnswer { invocation -> val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1)) as DiskFlushNotification thread { @@ -199,10 +200,10 @@ class UncaughtExceptionHandlerIntegrationTest { val sut = fixture.getSut(flushTimeoutMillis = 5000) - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) // shouldn't fall into timed out state, because we marked event as flushed on another thread verify(fixture.logger, never()).log( any(), @@ -213,14 +214,14 @@ class UncaughtExceptionHandlerIntegrationTest { @Test fun `does not block flushing when the event was dropped`() { - whenever(fixture.hub.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) + whenever(fixture.scopes.captureEvent(any(), any())).thenReturn(SentryId.EMPTY_ID) val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) // we do not call markFlushed, hence it should time out waiting for flush, but because // we drop the event, it should not even come to this if-check verify(fixture.logger, never()).log( @@ -233,17 +234,17 @@ class UncaughtExceptionHandlerIntegrationTest { @Test fun `waits for event to flush on disk if it was dropped by multithreaded deduplicator`() { val hintCaptor = argumentCaptor() - whenever(fixture.hub.captureEvent(any(), hintCaptor.capture())).thenAnswer { + whenever(fixture.scopes.captureEvent(any(), hintCaptor.capture())).thenAnswer { HintUtils.setEventDropReason(hintCaptor.firstValue, MULTITHREADED_DEDUPLICATION) return@thenAnswer SentryId.EMPTY_ID } val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent(any(), any()) + verify(fixture.scopes).captureEvent(any(), any()) // we do not call markFlushed, even though we dropped the event, the reason was // MULTITHREADED_DEDUPLICATION, so it should time out verify(fixture.logger).log( @@ -256,15 +257,15 @@ class UncaughtExceptionHandlerIntegrationTest { @Test fun `when there is no active transaction on scope, sets current event id as flushable`() { val eventCaptor = argumentCaptor() - whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + whenever(fixture.scopes.captureEvent(eventCaptor.capture(), any())) .thenReturn(SentryId.EMPTY_ID) val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), argThat { (HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) @@ -276,16 +277,16 @@ class UncaughtExceptionHandlerIntegrationTest { @Test fun `when there is active transaction on scope, does not set current event id as flushable`() { val eventCaptor = argumentCaptor() - whenever(fixture.hub.transaction).thenReturn(mock()) - whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + whenever(fixture.scopes.transaction).thenReturn(mock()) + whenever(fixture.scopes.captureEvent(eventCaptor.capture(), any())) .thenReturn(SentryId.EMPTY_ID) val sut = fixture.getSut() - sut.register(fixture.hub, fixture.options) + sut.register(fixture.scopes, fixture.options) sut.uncaughtException(fixture.thread, fixture.throwable) - verify(fixture.hub).captureEvent( + verify(fixture.scopes).captureEvent( any(), argThat { !(HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) @@ -307,10 +308,10 @@ class UncaughtExceptionHandlerIntegrationTest { } val integration1 = UncaughtExceptionHandlerIntegration(handler) - integration1.register(fixture.hub, fixture.options) + integration1.register(fixture.scopes, fixture.options) val integration2 = UncaughtExceptionHandlerIntegration(handler) - integration2.register(fixture.hub, fixture.options) + integration2.register(fixture.scopes, fixture.options) assertEquals(currentDefaultHandler, integration2) integration2.close() @@ -333,10 +334,10 @@ class UncaughtExceptionHandlerIntegrationTest { } val integration1 = UncaughtExceptionHandlerIntegration(handler) - integration1.register(fixture.hub, fixture.options) + integration1.register(fixture.scopes, fixture.options) val integration2 = UncaughtExceptionHandlerIntegration(handler) - integration2.register(fixture.hub, fixture.options) + integration2.register(fixture.scopes, fixture.options) assertEquals(currentDefaultHandler, integration2) integration2.close() diff --git a/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt b/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt index c010c972381..690ba54c147 100644 --- a/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt +++ b/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt @@ -1,11 +1,13 @@ package io.sentry.backpressure -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISentryExecutorService import io.sentry.SentryOptions import io.sentry.backpressure.BackpressureMonitor.MAX_DOWNSAMPLE_FACTOR +import org.mockito.ArgumentMatchers.eq import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.concurrent.Future @@ -17,13 +19,15 @@ class BackpressureMonitorTest { class Fixture { val options = SentryOptions() - val hub = mock() + val scopes = mock() val executor = mock() + val returnedFuture = mock>() fun getSut(): BackpressureMonitor { options.executorService = executor whenever(executor.isClosed).thenReturn(false) - whenever(executor.schedule(any(), any())).thenReturn(mock>()) - return BackpressureMonitor(options, hub) + whenever(executor.schedule(any(), any())).thenReturn(returnedFuture) + whenever(returnedFuture.cancel(any())).thenReturn(true) + return BackpressureMonitor(options, scopes) } } @@ -38,7 +42,7 @@ class BackpressureMonitorTest { @Test fun `downsampleFactor increases with negative health checks up to max`() { val sut = fixture.getSut() - whenever(fixture.hub.isHealthy).thenReturn(false) + whenever(fixture.scopes.isHealthy).thenReturn(false) assertEquals(0, sut.downsampleFactor) (1..MAX_DOWNSAMPLE_FACTOR).forEach { i -> @@ -54,13 +58,13 @@ class BackpressureMonitorTest { @Test fun `downsampleFactor goes back to 0 after positive health check`() { val sut = fixture.getSut() - whenever(fixture.hub.isHealthy).thenReturn(false) + whenever(fixture.scopes.isHealthy).thenReturn(false) assertEquals(0, sut.downsampleFactor) sut.checkHealth() assertEquals(1, sut.downsampleFactor) - whenever(fixture.hub.isHealthy).thenReturn(true) + whenever(fixture.scopes.isHealthy).thenReturn(true) sut.checkHealth() assertEquals(0, sut.downsampleFactor) } @@ -80,4 +84,17 @@ class BackpressureMonitorTest { verify(fixture.executor).schedule(any(), any()) } + + @Test + fun `close cancels latest job`() { + val sut = fixture.getSut() + sut.run() + + verify(fixture.executor).schedule(any(), any()) + verify(fixture.returnedFuture, never()).cancel(any()) + + sut.close() + + verify(fixture.returnedFuture).cancel(eq(true)) + } } diff --git a/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt b/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt index 3c3e6d18d06..0c8d6210540 100644 --- a/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt +++ b/sentry/src/test/java/io/sentry/cache/CacheStrategyTest.kt @@ -14,7 +14,6 @@ import java.io.ByteArrayInputStream import java.io.File import java.io.InputStreamReader import java.nio.file.Files -import java.util.UUID import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals @@ -166,7 +165,7 @@ class CacheStrategyTest { DateUtils.getDateTime("2020-02-07T14:16:00.000Z"), 2, "123", - UUID.fromString("c81d4e2e-bcf2-11e6-869b-7df92533d2db"), + "c81d4e2ebcf211e6869b7df92533d2db", init, 123456.toLong(), 6000.toDouble(), diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index 98751f8c448..7b4abe3f4f3 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -8,6 +8,7 @@ import io.sentry.SentryCrashLastRunState import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryOptions +import io.sentry.SentryUUID import io.sentry.Session import io.sentry.Session.State import io.sentry.Session.State.Ok @@ -24,7 +25,6 @@ import java.io.File import java.nio.file.Files import java.nio.file.Path import java.util.Date -import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test @@ -308,7 +308,7 @@ class EnvelopeCacheTest { DateUtils.getCurrentDateTime(), 0, "dis", - UUID.randomUUID(), + SentryUUID.generateSentryId(), true, null, null, diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportMultiThreadingTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportMultiThreadingTest.kt index 6cb22fc7efc..29809e56e6c 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportMultiThreadingTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportMultiThreadingTest.kt @@ -8,6 +8,7 @@ import io.sentry.SentryEnvelopeItem import io.sentry.SentryOptions import io.sentry.dsnString import io.sentry.protocol.SentryId +import io.sentry.test.initForTest import java.util.UUID import java.util.concurrent.ExecutorCompletionService import java.util.concurrent.Executors @@ -182,7 +183,7 @@ class ClientReportMultiThreadingTest { } private fun setupSentry(callback: Sentry.OptionsConfiguration? = null) { - Sentry.init { options -> + initForTest { options -> options.dsn = dsnString callback?.configure(options) opts = options diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index 135cce944be..23f086dd04c 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -7,7 +7,7 @@ import io.sentry.DataCategory import io.sentry.DateUtils import io.sentry.EventProcessor import io.sentry.Hint -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData import io.sentry.ReplayRecording @@ -26,10 +26,10 @@ import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.UserFeedback import io.sentry.dsnString import io.sentry.hints.Retryable -import io.sentry.metrics.EncodedMetrics import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.test.initForTest import io.sentry.util.HintUtils import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -51,9 +51,9 @@ class ClientReportTest { @Test fun `lost envelope can be recorded`() { givenClientReportRecorder() - val hub = mock() - whenever(hub.options).thenReturn(opts) - val transaction = SentryTracer(TransactionContext("name", "op"), hub) + val scopes = mock() + whenever(scopes.options).thenReturn(opts) + val transaction = SentryTracer(TransactionContext("name", "op"), scopes) val lostClientReport = ClientReport( DateUtils.getCurrentDateTime(), @@ -73,14 +73,13 @@ class ClientReportTest { SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000), SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer), SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)), - SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())), SentryEnvelopeItem.fromReplay(opts.serializer, opts.logger, SentryReplayEvent(), ReplayRecording(), false) ) clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope) val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() - testHelper.assertTotalCount(16, clientReportAtEnd) + testHelper.assertTotalCount(15, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) @@ -92,16 +91,15 @@ class ClientReportTest { testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Attachment, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Monitor, 1, clientReportAtEnd) - testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.MetricBucket, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Replay, 1, clientReportAtEnd) } @Test fun `lost transaction records dropped spans`() { givenClientReportRecorder() - val hub = mock() - whenever(hub.options).thenReturn(opts) - val transaction = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), hub) + val scopes = mock() + whenever(scopes.options).thenReturn(opts) + val transaction = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), scopes) transaction.startChild("lost span", "span1").finish() transaction.startChild("lost span", "span2").finish() transaction.startChild("lost span", "span3").finish() @@ -195,7 +193,7 @@ class ClientReportTest { } private fun setupSentry(callback: Sentry.OptionsConfiguration? = null) { - Sentry.init { options -> + initForTest { options -> options.dsn = dsnString callback?.configure(options) opts = options diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt index 00c89f27bea..f6271b5b5e8 100644 --- a/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt +++ b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt @@ -1,6 +1,6 @@ package io.sentry.instrumentation.file -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.ISpan import io.sentry.ITransaction import io.sentry.util.PlatformTestManipulator @@ -20,25 +20,25 @@ class FileIOSpanManagerTest { @Test fun `startSpan uses transaction on Android platform`() { - val hub = mock() + val scopes = mock() val transaction = mock() - whenever(hub.transaction).thenReturn(transaction) + whenever(scopes.transaction).thenReturn(transaction) PlatformTestManipulator.pretendIsAndroid(true) - FileIOSpanManager.startSpan(hub, "op.read") + FileIOSpanManager.startSpan(scopes, "op.read") verify(transaction).startChild(any()) } @Test fun `startSpan uses last span on non-Android platforms`() { - val hub = mock() + val scopes = mock() val span = mock() - whenever(hub.span).thenReturn(span) + whenever(scopes.span).thenReturn(span) PlatformTestManipulator.pretendIsAndroid(false) - FileIOSpanManager.startSpan(hub, "op.read") + FileIOSpanManager.startSpan(scopes, "op.read") verify(span).startChild(any()) } } diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileInputStreamTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileInputStreamTest.kt index 5e27eb451d3..def4e8c5559 100644 --- a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileInputStreamTest.kt +++ b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileInputStreamTest.kt @@ -1,6 +1,6 @@ package io.sentry.instrumentation.file -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention @@ -8,7 +8,7 @@ import io.sentry.SpanStatus import io.sentry.SpanStatus.INTERNAL_ERROR import io.sentry.TransactionContext import io.sentry.protocol.SentryStackFrame -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.ThreadChecker import org.awaitility.kotlin.await import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -23,13 +23,14 @@ import kotlin.concurrent.thread import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue class SentryFileInputStreamTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var sentryTracer: SentryTracer private val options = SentryOptions() @@ -40,21 +41,21 @@ class SentryFileInputStreamTest { sendDefaultPii: Boolean = false ): SentryFileInputStream { tmpFile?.writeText("Text") - whenever(hub.options).thenReturn( + whenever(scopes.options).thenReturn( options.apply { isSendDefaultPii = sendDefaultPii - mainThreadChecker = MainThreadChecker.getInstance() + threadChecker = ThreadChecker.getInstance() addInAppInclude("org.junit") } ) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (activeTransaction) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } return if (fileDescriptor == null) { - SentryFileInputStream(tmpFile, hub) + SentryFileInputStream(tmpFile, scopes) } else { - SentryFileInputStream(fileDescriptor, hub) + SentryFileInputStream(fileDescriptor, scopes) } } @@ -62,13 +63,13 @@ class SentryFileInputStreamTest { tmpFile: File? = null, delegate: FileInputStream ): SentryFileInputStream { - whenever(hub.options).thenReturn(options) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) + whenever(scopes.span).thenReturn(sentryTracer) return SentryFileInputStream.Factory.create( delegate, tmpFile, - hub + scopes ) as SentryFileInputStream } } @@ -80,6 +81,8 @@ class SentryFileInputStreamTest { private val tmpFile: File get() = tmpDir.newFile("test.txt") + private val tmpFileWithoutExtension: File get() = tmpDir.newFile("test") + @Test fun `when no active transaction does not capture a span`() { fixture.getSut(tmpFile, activeTransaction = false) @@ -104,13 +107,42 @@ class SentryFileInputStreamTest { assertEquals(fixture.sentryTracer.children.size, 1) val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (0 B)") assertEquals(fileIOSpan.data["file.size"], 0L) assertEquals(fileIOSpan.throwable, null) assertEquals(fileIOSpan.isFinished, true) assertEquals(fileIOSpan.status, SpanStatus.OK) } + @Test + fun `captures file name in description and file path when isSendDefaultPii is true`() { + val fis = fixture.getSut(tmpFile, sendDefaultPii = true) + fis.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "test.txt (0 B)") + assertNotNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file extension in description when isSendDefaultPii is false`() { + val fis = fixture.getSut(tmpFile, sendDefaultPii = false) + fis.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "***.txt (0 B)") + assertNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file size if no extension is available when isSendDefaultPii is false`() { + val fis = fixture.getSut(tmpFileWithoutExtension, sendDefaultPii = false) + fis.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "*** (0 B)") + assertNull(fileIOSpan.data["file.path"]) + } + @Test fun `when stream is closed, releases file descriptor`() { val fis = fixture.getSut(tmpFile) @@ -123,7 +155,7 @@ class SentryFileInputStreamTest { fixture.getSut(tmpFile).use { it.read() } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (1 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (1 B)") assertEquals(fileIOSpan.data["file.size"], 1L) } @@ -132,7 +164,7 @@ class SentryFileInputStreamTest { fixture.getSut(tmpFile).use { it.read(ByteArray(10)) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") assertEquals(fileIOSpan.data["file.size"], 4L) } @@ -141,7 +173,7 @@ class SentryFileInputStreamTest { fixture.getSut(tmpFile).use { it.read(ByteArray(10), 1, 3) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (3 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (3 B)") assertEquals(fileIOSpan.data["file.size"], 3L) } @@ -150,7 +182,7 @@ class SentryFileInputStreamTest { fixture.getSut(tmpFile).use { it.skip(10) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (10 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (10 B)") assertEquals(fileIOSpan.data["file.size"], 10L) } @@ -159,7 +191,7 @@ class SentryFileInputStreamTest { fixture.getSut(tmpFile).use { it.reader().readText() } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") assertEquals(fileIOSpan.data["file.size"], 4L) } diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileOutputStreamTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileOutputStreamTest.kt index f6a09830c26..f1826e5544b 100644 --- a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileOutputStreamTest.kt +++ b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileOutputStreamTest.kt @@ -1,13 +1,13 @@ package io.sentry.instrumentation.file -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext import io.sentry.protocol.SentryStackFrame -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.ThreadChecker import org.awaitility.kotlin.await import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -19,30 +19,32 @@ import kotlin.concurrent.thread import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue class SentryFileOutputStreamTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var sentryTracer: SentryTracer internal fun getSut( tmpFile: File? = null, activeTransaction: Boolean = true, - append: Boolean = false + append: Boolean = false, + optionsConfiguration: (SentryOptions) -> Unit = {} ): SentryFileOutputStream { - whenever(hub.options).thenReturn( - SentryOptions().apply { - mainThreadChecker = MainThreadChecker.getInstance() - addInAppInclude("org.junit") - } - ) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + val options = SentryOptions().apply { + threadChecker = ThreadChecker.getInstance() + addInAppInclude("org.junit") + optionsConfiguration(this) + } + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (activeTransaction) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } - return SentryFileOutputStream(tmpFile, append, hub) + return SentryFileOutputStream(tmpFile, append, scopes) } } @@ -53,6 +55,8 @@ class SentryFileOutputStreamTest { private val tmpFile: File get() = tmpDir.newFile("test.txt") + private val tmpFileWithoutExtension: File get() = tmpDir.newFile("test") + @Test fun `when no active transaction does not capture a span`() { fixture.getSut(tmpFile, activeTransaction = false) @@ -77,13 +81,49 @@ class SentryFileOutputStreamTest { assertEquals(fixture.sentryTracer.children.size, 1) val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (0 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (0 B)") assertEquals(fileIOSpan.data["file.size"], 0L) assertEquals(fileIOSpan.throwable, null) assertEquals(fileIOSpan.isFinished, true) assertEquals(fileIOSpan.status, SpanStatus.OK) } + @Test + fun `captures file name in description and file path when isSendDefaultPii is true`() { + val fos = fixture.getSut(tmpFile) { + it.isSendDefaultPii = true + } + fos.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "test.txt (0 B)") + assertNotNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file extension in description when isSendDefaultPii is false`() { + val fos = fixture.getSut(tmpFile) { + it.isSendDefaultPii = false + } + fos.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "***.txt (0 B)") + assertNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file size if no extension is available when isSendDefaultPii is false`() { + val fos = fixture.getSut(tmpFileWithoutExtension) { + it.isSendDefaultPii = false + } + fos.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "*** (0 B)") + assertNull(fileIOSpan.data["file.path"]) + } + @Test fun `when stream is closed file descriptor is also closed`() { val fos = fixture.getSut(tmpFile) @@ -96,7 +136,7 @@ class SentryFileOutputStreamTest { fixture.getSut(tmpFile).use { it.write(29) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (1 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (1 B)") assertEquals(fileIOSpan.data["file.size"], 1L) } @@ -105,7 +145,7 @@ class SentryFileOutputStreamTest { fixture.getSut(tmpFile).use { it.write(ByteArray(10)) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (10 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (10 B)") assertEquals(fileIOSpan.data["file.size"], 10L) } @@ -114,7 +154,7 @@ class SentryFileOutputStreamTest { fixture.getSut(tmpFile).use { it.write(ByteArray(10), 1, 3) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (3 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (3 B)") assertEquals(fileIOSpan.data["file.size"], 3L) } @@ -123,7 +163,7 @@ class SentryFileOutputStreamTest { fixture.getSut(tmpFile).use { it.write("Text".toByteArray()) } val fileIOSpan = fixture.sentryTracer.children.first() - assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") assertEquals(fileIOSpan.data["file.size"], 4L) } diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileReaderTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileReaderTest.kt index 2485579e7a9..0f8240e301a 100644 --- a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileReaderTest.kt +++ b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileReaderTest.kt @@ -1,12 +1,12 @@ package io.sentry.instrumentation.file -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus.OK import io.sentry.TransactionContext -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.ThreadChecker import org.junit.Rule import org.junit.rules.TemporaryFolder import org.mockito.kotlin.mock @@ -14,27 +14,30 @@ import org.mockito.kotlin.whenever import java.io.File import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class SentryFileReaderTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var sentryTracer: SentryTracer internal fun getSut( tmpFile: File, - activeTransaction: Boolean = true + activeTransaction: Boolean = true, + optionsConfiguration: (SentryOptions) -> Unit = {} ): SentryFileReader { tmpFile.writeText("TEXT") - whenever(hub.options).thenReturn( - SentryOptions().apply { - mainThreadChecker = MainThreadChecker.getInstance() - } - ) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + val options = SentryOptions().apply { + threadChecker = ThreadChecker.getInstance() + optionsConfiguration(this) + } + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (activeTransaction) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } - return SentryFileReader(tmpFile, hub) + return SentryFileReader(tmpFile, scopes) } } @@ -45,6 +48,8 @@ class SentryFileReaderTest { private val tmpFile: File get() = tmpDir.newFile("test.txt") + private val tmpFileWithoutExtension: File get() = tmpDir.newFile("test") + @Test fun `captures a span`() { val reader = fixture.getSut(tmpFile) @@ -54,11 +59,50 @@ class SentryFileReaderTest { assertEquals(fixture.sentryTracer.children.size, 1) val fileIOSpan = fixture.sentryTracer.children.first() assertEquals(fileIOSpan.spanContext.operation, "file.read") - assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") assertEquals(fileIOSpan.data["file.size"], 4L) assertEquals(fileIOSpan.throwable, null) assertEquals(fileIOSpan.isFinished, true) assertEquals(fileIOSpan.data[SpanDataConvention.BLOCKED_MAIN_THREAD_KEY], true) assertEquals(fileIOSpan.status, OK) } + + @Test + fun `captures file name in description and file path when isSendDefaultPii is true`() { + val reader = fixture.getSut(tmpFile) { + it.isSendDefaultPii = true + } + reader.readText() + reader.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertNotNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file extension in description when isSendDefaultPii is false`() { + val reader = fixture.getSut(tmpFile) { + it.isSendDefaultPii = false + } + reader.readText() + reader.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") + assertNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file size if no extension is available when isSendDefaultPii is false`() { + val reader = fixture.getSut(tmpFileWithoutExtension) { + it.isSendDefaultPii = false + } + reader.readText() + reader.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "*** (4 B)") + assertNull(fileIOSpan.data["file.path"]) + } } diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileWriterTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileWriterTest.kt index f0738d87258..8c8f2238d4f 100644 --- a/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileWriterTest.kt +++ b/sentry/src/test/java/io/sentry/instrumentation/file/SentryFileWriterTest.kt @@ -1,12 +1,12 @@ package io.sentry.instrumentation.file -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus.OK import io.sentry.TransactionContext -import io.sentry.util.thread.MainThreadChecker +import io.sentry.util.thread.ThreadChecker import org.junit.Rule import org.junit.rules.TemporaryFolder import org.mockito.kotlin.mock @@ -14,27 +14,30 @@ import org.mockito.kotlin.whenever import java.io.File import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class SentryFileWriterTest { class Fixture { - val hub = mock() + val scopes = mock() lateinit var sentryTracer: SentryTracer internal fun getSut( tmpFile: File, activeTransaction: Boolean = true, - append: Boolean = false + append: Boolean = false, + optionsConfiguration: (SentryOptions) -> Unit = {} ): SentryFileWriter { - whenever(hub.options).thenReturn( - SentryOptions().apply { - mainThreadChecker = MainThreadChecker.getInstance() - } - ) - sentryTracer = SentryTracer(TransactionContext("name", "op"), hub) + val options = SentryOptions().apply { + threadChecker = ThreadChecker.getInstance() + optionsConfiguration(this) + } + whenever(scopes.options).thenReturn(options) + sentryTracer = SentryTracer(TransactionContext("name", "op"), scopes) if (activeTransaction) { - whenever(hub.span).thenReturn(sentryTracer) + whenever(scopes.span).thenReturn(sentryTracer) } - return SentryFileWriter(tmpFile, append, hub) + return SentryFileWriter(tmpFile, append, scopes) } } @@ -45,6 +48,8 @@ class SentryFileWriterTest { private val tmpFile: File by lazy { tmpDir.newFile("test.txt") } + private val tmpFileWithoutExtension: File by lazy { tmpDir.newFile("test") } + @Test fun `captures a span`() { val writer = fixture.getSut(tmpFile) @@ -54,7 +59,7 @@ class SentryFileWriterTest { assertEquals(fixture.sentryTracer.children.size, 1) val fileIOSpan = fixture.sentryTracer.children.first() assertEquals(fileIOSpan.spanContext.operation, "file.write") - assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") assertEquals(fileIOSpan.data["file.size"], 4L) assertEquals(fileIOSpan.throwable, null) assertEquals(fileIOSpan.isFinished, true) @@ -77,4 +82,43 @@ class SentryFileWriterTest { assertEquals("testtest2", tmpFile.readText()) } + + @Test + fun `captures file name in description and file path when isSendDefaultPii is true`() { + val writer = fixture.getSut(tmpFile) { + it.isSendDefaultPii = true + } + writer.write("TEXT") + writer.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "test.txt (4 B)") + assertNotNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file extension in description when isSendDefaultPii is false`() { + val writer = fixture.getSut(tmpFile) { + it.isSendDefaultPii = false + } + writer.write("TEXT") + writer.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "***.txt (4 B)") + assertNull(fileIOSpan.data["file.path"]) + } + + @Test + fun `captures only file size if no extension is available when isSendDefaultPii is false`() { + val writer = fixture.getSut(tmpFileWithoutExtension) { + it.isSendDefaultPii = false + } + writer.write("TEXT") + writer.close() + + val fileIOSpan = fixture.sentryTracer.children.first() + assertEquals(fileIOSpan.spanContext.description, "*** (4 B)") + assertNull(fileIOSpan.data["file.path"]) + } } diff --git a/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt b/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt index 73aa339f261..0a91bec562a 100644 --- a/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/internal/SpotlightIntegrationTest.kt @@ -1,6 +1,6 @@ package io.sentry.internal -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryOptions import io.sentry.SentryOptions.BeforeEnvelopeCallback import io.sentry.SpotlightIntegration @@ -19,7 +19,7 @@ class SpotlightIntegrationTest { } val spotlight = SpotlightIntegration() - spotlight.register(mock(), options) + spotlight.register(mock(), options) assertNull(options.beforeEnvelopeCallback) } @@ -33,7 +33,7 @@ class SpotlightIntegrationTest { } val spotlight = SpotlightIntegration() - spotlight.register(mock(), options) + spotlight.register(mock(), options) assertEquals(envelopeCallback, options.beforeEnvelopeCallback) } @@ -45,7 +45,7 @@ class SpotlightIntegrationTest { } val spotlight = SpotlightIntegration() - spotlight.register(mock(), options) + spotlight.register(mock(), options) assertEquals(options.beforeEnvelopeCallback, spotlight) spotlight.close() @@ -71,7 +71,7 @@ class SpotlightIntegrationTest { } val spotlight = SpotlightIntegration() - spotlight.register(mock(), options) + spotlight.register(mock(), options) assertEquals("http://example.com:1234/stream", spotlight.spotlightConnectionUrl) } diff --git a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt deleted file mode 100644 index b1af3faa1f7..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals - -class CounterMetricTest { - - @Test - fun add() { - val metric = CounterMetric( - "test", - 1.0, - null, - mapOf( - "tag1" to "value1", - "tag2" to "value2" - ) - ) - assertEquals(1.0, metric.value) - - metric.add(2.0) - assertEquals(3.0, metric.value) - - // TODO should we allow negative values? - // TODO should we do any bounds checks? - metric.add(-3.0) - assertEquals(0.0, metric.value) - } - - @Test - fun type() { - val metric = CounterMetric( - "test", - 1.0, - null, - mapOf( - "tag1" to "value1", - "tag2" to "value2" - ) - ) - assertEquals(MetricType.Counter, metric.type) - } - - @Test - fun weight() { - val metric = CounterMetric( - "test", - 1.0, - null, - mapOf( - "tag1" to "value1", - "tag2" to "value2" - ) - ) - assertEquals(1, metric.weight) - } - - @Test - fun values() { - val metric = CounterMetric( - "test", - 1.0, - null, - mapOf( - "tag1" to "value1", - "tag2" to "value2" - ) - ) - - val values0 = metric.serialize().toList() - assertEquals(1, values0.size) - assertEquals(1.0, values0[0] as Double) - - metric.add(1.0) - val values1 = metric.serialize().toList() - assertEquals(1, values1.size) - assertEquals(2.0, values1[0] as Double) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt deleted file mode 100644 index 13c74105f0f..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals - -class DistributionMetricTest { - - @Test - fun add() { - val metric = DistributionMetric( - "test", - 1.0, - null, - null - ) - assertEquals(listOf(1.0), metric.serialize().toList()) - - metric.add(1.0) - metric.add(2.0) - assertEquals(listOf(1.0, 1.0, 2.0), metric.serialize().toList()) - } - - @Test - fun type() { - val metric = DistributionMetric( - "test", - 1.0, - null, - null - ) - assertEquals(MetricType.Distribution, metric.type) - } - - @Test - fun weight() { - val metric = DistributionMetric( - "test", - 1.0, - null, - null - ) - assertEquals(1, metric.weight) - - metric.add(2.0) - assertEquals(2, metric.weight) - } - - @Test - fun values() { - val metric = DistributionMetric( - "test", - 1.0, - null, - null - ) - metric.add(2.0) - - val values = metric.serialize().toList() - assertEquals(2, values.size) - assertEquals(listOf(1.0, 2.0), values) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt deleted file mode 100644 index 73852717775..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals - -class GaugeMetricTest { - - @Test - fun add() { - val metric = GaugeMetric( - "test", - 1.0, - null, - null - ) - assertEquals( - listOf( - 1.0, - 1.0, - 1.0, - 1.0, - 1 - ), - metric.serialize().toList() - ) - - metric.add(5.0) - metric.add(4.0) - metric.add(3.0) - metric.add(2.0) - metric.add(1.0) - assertEquals( - listOf( - 1.0, // last - 1.0, // min - 5.0, // max - 16.0, // sum - 6 // count - ), - metric.serialize().toList() - ) - } - - @Test - fun type() { - val metric = GaugeMetric( - "test", - 1.0, - null, - null - ) - assertEquals(MetricType.Gauge, metric.type) - } - - @Test - fun weight() { - val metric = GaugeMetric( - "test", - 1.0, - null, - null - ) - assertEquals(5, metric.weight) - - // even when values are added, the weight is still 5 - metric.add(2.0) - assertEquals(5, metric.weight) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt deleted file mode 100644 index 83c5d3864d5..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/LocalMetricsAggregatorTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull - -class LocalMetricsAggregatorTest { - @Test - fun `metrics are grouped by export key`() { - val aggregator = LocalMetricsAggregator() - - val type = MetricType.Counter - val key = "op.count" - val unit = null - val tags0 = mapOf( - "tag" to "value0" - ) - - // when a metric is emitted - aggregator.add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags0), - type, - key, - 1.0, - unit, - tags0 - ) - - // and the same metric is emitted with different tags - val tags1 = mapOf( - "tag" to "value1" - ) - aggregator.add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags1), - type, - key, - 1.0, - unit, - tags1 - ) - - // then the summary contain a single top level group for the metric - assertEquals(1, aggregator.summaries.size) - assertNotNull(aggregator.summaries[MetricsHelper.getExportKey(type, key, unit)]) - - // and 2 summaries based on the different tags should be present - val metricSummaries = aggregator.summaries[MetricsHelper.getExportKey(type, key, unit)]!! - assertEquals(2, metricSummaries.size) - } - - @Test - fun `metrics are aggregated`() { - val aggregator = LocalMetricsAggregator() - - val type = MetricType.Counter - val key = "op.count" - val unit = null - val tags = mapOf( - "tag0" to "value0" - ) - val timestamp = 0L - - // when a metric is emitted two times - aggregator.add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags), - type, - key, - 1.0, - unit, - tags - ) - - aggregator.add( - MetricsHelper.getMetricBucketKey(type, key, unit, tags), - type, - key, - 2.0, - unit, - tags - ) - - val metric = aggregator.summaries.values.first()[0] - assertEquals(1.0, metric.min) - assertEquals(2.0, metric.max) - assertEquals(3.0, metric.sum) - assertEquals(2, metric.count) - assertEquals(tags, metric.tags) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt deleted file mode 100644 index 24ef6ffa5bb..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt +++ /dev/null @@ -1,385 +0,0 @@ -package io.sentry.metrics - -import io.sentry.DateUtils -import io.sentry.IMetricsAggregator -import io.sentry.ISpan -import io.sentry.MeasurementUnit -import io.sentry.SentryNanotimeDate -import io.sentry.metrics.MetricsApi.IMetricsInterface -import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import kotlin.test.Test -import kotlin.test.assertEquals - -class MetricsApiTest { - - class Fixture { - val aggregator = mock() - val localMetricsAggregator = mock() - - var lastSpan: ISpan? = null - var lastOp: String? = null - var lastDescription: String? = null - - fun getSut( - defaultTags: Map = emptyMap(), - spanProvider: () -> ISpan? = { - val span = mock() - val date = SentryNanotimeDate() - whenever(span.startDate).thenReturn(date) - span - } - ): MetricsApi { - val localAggregator = localMetricsAggregator - - return MetricsApi(object : IMetricsInterface { - override fun getMetricsAggregator(): IMetricsAggregator { - return aggregator - } - - override fun getLocalMetricsAggregator(): LocalMetricsAggregator? = localAggregator - - override fun getDefaultTagsForMetrics(): Map = defaultTags - - override fun startSpanForMetric(op: String, description: String): ISpan? { - lastOp = op - lastDescription = description - lastSpan = spanProvider() - return lastSpan - } - }) - } - } - - val fixture = Fixture() - - @Test - fun `default timestamp is provided`() { - val api = fixture.getSut() - - api.increment("name", 1.0, null, null, null) - api.set("name", 1, null, null, null) - api.set("name", "string", null, null, null) - api.gauge("name", 1.0, null, null, null) - api.distribution("name", 1.0, null, null, null) - - verify(fixture.aggregator).increment( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - any(), - anyOrNull() - ) - - verify(fixture.aggregator).set( - anyOrNull(), - eq(1), - anyOrNull(), - anyOrNull(), - any(), - anyOrNull() - ) - - verify(fixture.aggregator).set( - anyOrNull(), - eq("string"), - anyOrNull(), - anyOrNull(), - any(), - anyOrNull() - ) - - verify(fixture.aggregator).gauge( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - any(), - anyOrNull() - ) - - verify(fixture.aggregator).distribution( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - any(), - anyOrNull() - ) - } - - @Test - fun `timestamp is not overwritten`() { - val api = fixture.getSut() - - api.increment("name", 1.0, null, null, 1234) - api.set("name", 1, null, null, 1234) - api.set("name", "string", null, null, 1234) - api.gauge("name", 1.0, null, null, 1234) - api.distribution("name", 1.0, null, null, 1234) - - verify(fixture.aggregator).increment( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(1234), - anyOrNull() - ) - - verify(fixture.aggregator).set( - anyOrNull(), - eq(1), - anyOrNull(), - anyOrNull(), - eq(1234), - anyOrNull() - ) - - verify(fixture.aggregator).set( - anyOrNull(), - eq("string"), - anyOrNull(), - anyOrNull(), - eq(1234), - anyOrNull() - ) - - verify(fixture.aggregator).gauge( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(1234), - anyOrNull() - ) - - verify(fixture.aggregator).distribution( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(1234), - anyOrNull() - ) - } - - @Test - fun `tags are enriched with default tags`() { - val api = fixture.getSut( - defaultTags = mapOf( - "release" to "1.0", - "environment" to "prod" - ) - ) - - api.increment("name", 1.0, null, mapOf("a" to "b")) - - verify(fixture.aggregator).increment( - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq( - mapOf( - "a" to "b", - "release" to "1.0", - "environment" to "prod" - ) - ), - anyOrNull(), - anyOrNull() - ) - } - - @Test - fun `existing environment and release tags are not overwritten`() { - val api = fixture.getSut( - defaultTags = mapOf( - "release" to "1.0", - "environment" to "prod" - ) - ) - - api.increment( - "name", - 1.0, - null, - mapOf( - "release" to "2.0", - "environment" to "dev" - ) - ) - - verify(fixture.aggregator).increment( - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq( - mapOf( - "release" to "2.0", - "environment" to "dev" - ) - ), - anyOrNull(), - anyOrNull() - ) - } - - @Test - fun `local aggregator is provided to aggregator`() { - val api = fixture.getSut() - - api.increment("increment") - verify(fixture.aggregator).increment( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(fixture.localMetricsAggregator) - ) - - api.set("set", 1) - verify(fixture.aggregator).set( - anyOrNull(), - eq(1), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(fixture.localMetricsAggregator) - ) - - api.set("set", "string") - verify(fixture.aggregator).set( - anyOrNull(), - eq("string"), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(fixture.localMetricsAggregator) - ) - - api.gauge("gauge", 1.0) - verify(fixture.aggregator).gauge( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(fixture.localMetricsAggregator) - ) - - api.distribution("distribution", 1.0) - verify(fixture.aggregator).distribution( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(fixture.localMetricsAggregator) - ) - - api.timing("timing") { - // no-op - } - val spanLocalAggregator = fixture.lastSpan!!.localMetricsAggregator - verify(fixture.aggregator).distribution( - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(spanLocalAggregator) - ) - } - - @Test - fun `timing starts and finishes a span`() { - val api = fixture.getSut() - - api.timing("key") { - // no-op - } - - assertEquals("metric.timing", fixture.lastOp) - assertEquals("key", fixture.lastDescription) - - verify(fixture.lastSpan!!).finish() - } - - @Test - fun `timing value equals span duration`() { - val startDate = SentryNanotimeDate() - val finishNanos = startDate.nanoTimestamp() + 10000 - val finishDate = SentryNanotimeDate(DateUtils.nanosToDate(finishNanos), finishNanos) - val localAggregator = mock() - val span = mock() - whenever(span.startDate).thenReturn(startDate) - whenever(span.finishDate).thenReturn(finishDate) - whenever(span.localMetricsAggregator).thenReturn(localAggregator) - - val api = fixture.getSut(spanProvider = { span }) - - api.timing("key") { - // no-op - } - - assertEquals("metric.timing", fixture.lastOp) - assertEquals("key", fixture.lastDescription) - verify(fixture.aggregator).distribution( - eq("key"), - eq((finishDate.diff(startDate)) / 1000000000.0), - eq(MeasurementUnit.Duration.SECOND), - anyOrNull(), - anyOrNull(), - eq(localAggregator) - ) - } - - @Test - fun `timing applies metric tags as span tags`() { - val api = fixture.getSut(defaultTags = mapOf("release" to "1.0")) - // when timing is called - api.timing("key", { - // no-op - }, MeasurementUnit.Duration.NANOSECOND, mapOf("a" to "b")) - - // the last span should have the metric tags, without the default ones - verify(fixture.lastSpan!!, never()).setTag("release", "1.0") - verify(fixture.lastSpan!!).setTag("a", "b") - } - - @Test - fun `if timing throws an exception, span still finishes`() { - val api = fixture.getSut() - - try { - api.timing("key") { - throw IllegalStateException() - } - } catch (e: IllegalStateException) { - // ignored - } - - assertEquals("metric.timing", fixture.lastOp) - assertEquals("key", fixture.lastDescription) - verify(fixture.lastSpan!!).finish() - } - - @Test - fun `if timing does retrieve a null span, it still works`() { - val api = fixture.getSut( - spanProvider = { null } - ) - api.timing("key") { - // no-op - } - // no crash - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt deleted file mode 100644 index 8112170f68a..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -package io.sentry.metrics - -import io.sentry.MeasurementUnit -import kotlin.test.Test -import kotlin.test.assertEquals - -class MetricsHelperTest { - - companion object { - - data class StatsDMetric( - val timestamp: Long?, - val name: String?, - val unit: String?, - val type: String?, - val values: List?, - val tags: Map? - ) - - fun parseMetrics(byteArray: ByteArray): List { - val metrics = mutableListOf() - - val encodedMetrics = byteArray.decodeToString() - for (line in encodedMetrics.split("\n")) { - if (line.isEmpty()) { - continue - } - - val pieces = line.split("|") - val payload = pieces[0].split(":") - - val nameAndUnit = payload[0].split("@", limit = 2) - val name = nameAndUnit[0] - val unit = if (nameAndUnit.size == 2) nameAndUnit[1] else null - - val values = payload.subList(1, payload.size) - val type = pieces[1] - - var timestamp: Long? = null - val tags = mutableMapOf() - - for (piece in pieces.subList(2, pieces.size)) { - if (piece[0] == '#') { - for (pair in piece.substring(1, piece.length).split(",")) { - val (k, v) = pair.split(":", limit = 2) - tags[k] = v - } - } else if (piece[0] == 'T') { - timestamp = piece.substring(1, piece.length).toLong() - } else { - throw IllegalArgumentException("unknown piece $piece") - } - } - metrics.add(StatsDMetric(timestamp, name, unit, type, values, tags)) - } - metrics.sortBy { it.timestamp } - return metrics - } - } - - @Test - fun sanitizeName() { - assertEquals("foo-bar", MetricsHelper.sanitizeName("foo-bar")) - assertEquals("foo_bar", MetricsHelper.sanitizeName("foo\$\$\$bar")) - assertEquals("fo_-bar", MetricsHelper.sanitizeName("foö-bar")) - } - - @Test - fun sanitizeTagKey() { - // no replacement characters for tag keys - // - and / should be allowed - assertEquals("a/weird/tag-key/", MetricsHelper.sanitizeTagKey("a/weird/tag-key/:ä")) - } - - @Test - fun sanitizeTagValue() { - // https://github.com/getsentry/relay/blob/3208e3ce5b1fe4d147aa44e0e966807c256993de/relay-metrics/src/protocol.rs#L142 - assertEquals("plain", MetricsHelper.sanitizeTagValue("plain")) - assertEquals("plain text", MetricsHelper.sanitizeTagValue("plain text")) - assertEquals("plain%text", MetricsHelper.sanitizeTagValue("plain%text")) - - // Escape sequences - assertEquals("plain \\\\ text", MetricsHelper.sanitizeTagValue("plain \\ text")) - assertEquals("plain\\u{2c}text", MetricsHelper.sanitizeTagValue("plain,text")) - assertEquals("plain\\u{7c}text", MetricsHelper.sanitizeTagValue("plain|text")) - assertEquals("plain 😅", MetricsHelper.sanitizeTagValue("plain 😅")) - - // Escapable control characters (may be stripped by the parser) - assertEquals("plain\\ntext", MetricsHelper.sanitizeTagValue("plain\ntext")) - assertEquals("plain\\rtext", MetricsHelper.sanitizeTagValue("plain\rtext")) - assertEquals("plain\\ttext", MetricsHelper.sanitizeTagValue("plain\ttext")) - - // Unescapable control characters remain, as they'll be stripped by relay - assertEquals("plain\u0007text", MetricsHelper.sanitizeTagValue("plain\u0007text")) - assertEquals("plain\u009Ctext", MetricsHelper.sanitizeTagValue("plain\u009ctext")) - } - - @Test - fun sanitizeUnit() { - // no replacement characters for units - assertEquals("abcABC123_abcABC123", MetricsHelper.sanitizeUnit("abcABC123_-./äöü\$%&abcABC123")) - } - - @Test - fun getTimeBucketKey() { - assertEquals( - 0, - MetricsHelper.getTimeBucketKey(5000) - ) - - assertEquals( - -1, - MetricsHelper.getTimeBucketKey(-5000) - ) - - assertEquals( - 10, - MetricsHelper.getTimeBucketKey(10_000) - ) - - assertEquals( - 10, - MetricsHelper.getTimeBucketKey(10_001) - ) - - assertEquals( - 20, - MetricsHelper.getTimeBucketKey(20_000) - ) - - assertEquals( - 20, - MetricsHelper.getTimeBucketKey(29_999) - ) - - assertEquals( - 30, - MetricsHelper.getTimeBucketKey(30_000) - ) - } - - @Test - fun encode() { - val stringBuilder = StringBuilder() - MetricsHelper.encodeMetrics( - 1000, - listOf( - CounterMetric( - "name", - 1.0, - MeasurementUnit.Custom("oranges"), - mapOf( - "tag1" to "value1", - "tag2" to "value2" - ) - ) - ), - stringBuilder - ) - - val metrics = parseMetrics(stringBuilder.toString().toByteArray()) - - assertEquals(1, metrics.size) - - assertEquals( - StatsDMetric( - 1000, - "name", - "oranges", - "c", - listOf("1.0"), - mapOf("tag1" to "value1", "tag2" to "value2") - ), - metrics[0] - ) - } - - @Test - fun toStatsdType() { - assertEquals("c", MetricType.Counter.statsdCode) - assertEquals("g", MetricType.Gauge.statsdCode) - assertEquals("s", MetricType.Set.statsdCode) - assertEquals("d", MetricType.Distribution.statsdCode) - } - - @Test - fun exportKey() { - assertEquals("d:custom/background_operation@second", MetricsHelper.getExportKey(MetricType.Distribution, "custom/background_operation", MeasurementUnit.Duration.SECOND)) - assertEquals("d:custom/background_operation@none", MetricsHelper.getExportKey(MetricType.Distribution, "custom/background_operation", null)) - assertEquals("c:count@none", MetricsHelper.getExportKey(MetricType.Counter, "count", null)) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsIntegrationTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsIntegrationTest.kt deleted file mode 100644 index 56d40f5b86d..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/MetricsIntegrationTest.kt +++ /dev/null @@ -1,122 +0,0 @@ -package io.sentry.metrics - -import io.sentry.MetricsAggregator -import io.sentry.Sentry -import io.sentry.SentryClient -import io.sentry.SentryOptions -import io.sentry.TransactionOptions -import junit.framework.TestCase.assertEquals -import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.check -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import kotlin.test.Test - -class MetricsIntegrationTest { - - @Test - fun `metrics are collected`() { - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - release = "io.sentry.samples@2.3.0" - enableTracing = true - sampleRate = 1.0 - isEnableMetrics = true - } - Sentry.init(options) - - val client = mock() - val aggregator = MetricsAggregator(options, client) - whenever(client.metricsAggregator).thenReturn(aggregator) - Sentry.bindClient(client) - - // when metrics are emitted - Sentry.metrics().increment("counter", 1.0) - Sentry.metrics().increment("counter", 2.0) - Sentry.metrics().increment("counter", 3.0) - - // and sentry is flushed - // Sentry.close() would invoke client.close(), which calls aggregator.close() - // but our client is mocked - aggregator.close() - - // the aggregated metric should be captured - verify(client).captureMetrics( - check { - assertEquals(1, it.buckets.size) - val metric = it.buckets.values.first().values.first() - assertEquals("counter", metric.key) - assertEquals(listOf(6.0), metric.serialize().toList()) - } - ) - } - - @Test - fun `metric summaries are attached to txn and spans`() { - // ---- time ------> - // |-------- txn -------------| - // |-- span --| - // 1.0 2.0 3.0 - - // given an initialized SDK - val options = SentryOptions().apply { - dsn = "https://key@sentry.io/proj" - release = "io.sentry.samples@2.3.0" - enableTracing = true - sampleRate = 1.0 - isEnableMetrics = true - } - Sentry.init(options) - - val client = mock() - val aggregator = MetricsAggregator(options, client) - whenever(client.metricsAggregator).thenReturn(aggregator) - Sentry.bindClient(client) - - // when a txn starts - val txn = Sentry.startTransaction( - "name", - "op.load", - TransactionOptions().apply { - isBindToScope = true - } - ) - - // inc 1.0 happens on txn - Sentry.metrics().increment("counter", 1.0) - - // and a span starts - val span = txn.startChild("op.child") - - // inc 2.0 happens on span - Sentry.metrics().increment("counter", 2.0) - span.finish() - - // inc 3.0 happens on txn again, as the span is already finished - Sentry.metrics().increment("counter", 3.0) - txn.finish() - - Sentry.flush(0) - Sentry.close() - - // then the txn and span have the right summary - verify(client).captureTransaction( - check { - assertEquals(1, it.metricSummaries!!.size) - val txnSummary = it.metricSummaries!!.values.first().first() - assertEquals(2, txnSummary.count) - assertEquals(4.0, txnSummary.sum) - - assertEquals(1, it.spans[0].metricsSummaries!!.size) - val spanSummary = it.spans[0].metricsSummaries!!.values.first().first() - assertEquals(1, spanSummary.count) - assertEquals(2.0, spanSummary.sum) - }, - anyOrNull(), - anyOrNull(), - anyOrNull(), - anyOrNull() - ) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt deleted file mode 100644 index b41d9c2e1ca..00000000000 --- a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SetMetricTest { - - @Test - fun add() { - val metric = SetMetric( - "test", - null, - null - ) - assertTrue(metric.serialize().toList().isEmpty()) - - metric.add(1.0) - metric.add(2.0) - metric.add(3.0) - - assertEquals(3, metric.serialize().toList().size) - - // when an already existing item is added - // size stays the same - metric.add(3.0) - assertEquals(3, metric.serialize().toList().size) - } - - @Test - fun type() { - val metric = SetMetric( - "test", - null, - null - ) - assertEquals(MetricType.Set, metric.type) - } - - @Test - fun weight() { - val metric = SetMetric( - "test", - null, - null - ) - assertEquals(0, metric.weight) - - metric.add(1.0) - metric.add(2.0) - metric.add(3.0) - metric.add(3.0) - - // weight should be the number of distinct items - assertEquals(3, metric.weight) - } -} diff --git a/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt new file mode 100644 index 00000000000..cafcbb8884b --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/CombinedContextsViewSerializationTest.kt @@ -0,0 +1,89 @@ +package io.sentry.protocol + +import io.sentry.CombinedContextsView +import io.sentry.ILogger +import io.sentry.JsonObjectWriter +import io.sentry.ScopeType +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals + +class CombinedContextsViewSerializationTest { + + class Fixture { + val logger = mock() + + fun getSut(): CombinedContextsView { + val current = Contexts() + val isolation = Contexts() + val global = Contexts() + val combined = CombinedContextsView(global, isolation, current, ScopeType.ISOLATION) + + current.setApp(AppSerializationTest.Fixture().getSut()) + current.setBrowser(BrowserSerializationTest.Fixture().getSut()) + current.setTrace(SpanContextSerializationTest.Fixture().getSut()) + + isolation.setDevice(DeviceSerializationTest.Fixture().getSut()) + isolation.setOperatingSystem(OperatingSystemSerializationTest.Fixture().getSut()) + isolation.setResponse(ResponseSerializationTest.Fixture().getSut()) + + global.setRuntime(SentryRuntimeSerializationTest.Fixture().getSut()) + global.setGpu(GpuSerializationTest.Fixture().getSut()) + + return combined + } + } + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = SerializationUtils.sanitizedFile("json/contexts.json") + val actual = SerializationUtils.serializeToString(fixture.getSut(), fixture.logger) + + assertEquals(expected, actual) + } + + @Test + fun serializeUnknownEntry() { + val sut = fixture.getSut() + sut["fixture-key"] = "fixture-value" + + val writer = mock().apply { + whenever(name(any())).thenReturn(this) + } + sut.serialize(writer, fixture.logger) + + verify(writer).name("fixture-key") + verify(writer).value(fixture.logger, "fixture-value") + } + + @Test + fun deserialize() { + val expectedJson = SerializationUtils.sanitizedFile("json/contexts.json") + val actual = SerializationUtils.deserializeJson( + expectedJson, + Contexts.Deserializer(), + fixture.logger + ) + val actualJson = SerializationUtils.serializeToString(actual, fixture.logger) + + assertEquals(expectedJson, actualJson) + } + + @Test + fun deserializeUnknownEntry() { + val sut = fixture.getSut() + sut["fixture-key"] = "fixture-value" + val serialized = SerializationUtils.serializeToString(sut, fixture.logger) + val deserialized = SerializationUtils.deserializeJson( + serialized, + Contexts.Deserializer(), + fixture.logger + ) + + assertEquals("fixture-value", deserialized["fixture-key"]) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt index ee674790d57..5c9aeb1d375 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsSerializationTest.kt @@ -22,7 +22,7 @@ class ContextsSerializationTest { setRuntime(SentryRuntimeSerializationTest.Fixture().getSut()) setGpu(GpuSerializationTest.Fixture().getSut()) setResponse(ResponseSerializationTest.Fixture().getSut()) - trace = SpanContextSerializationTest.Fixture().getSut() + setTrace(SpanContextSerializationTest.Fixture().getSut()) } } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index c1fb47b1c7f..1d0573741fe 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -18,7 +18,7 @@ class ContextsTest { contexts.setRuntime(SentryRuntime()) contexts.setGpu(Gpu()) contexts.setResponse(Response()) - contexts.trace = SpanContext("op") + contexts.setTrace(SpanContext("op")) val clone = Contexts(contexts) @@ -38,7 +38,7 @@ class ContextsTest { fun `copying contexts will have the same values`() { val contexts = Contexts() contexts["some-property"] = "some-value" - contexts.trace = SpanContext("op") + contexts.setTrace(SpanContext("op")) contexts.trace!!.description = "desc" val clone = Contexts(contexts) diff --git a/sentry/src/test/java/io/sentry/protocol/DeviceSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/DeviceSerializationTest.kt index d09bed0643a..8081ad5db90 100644 --- a/sentry/src/test/java/io/sentry/protocol/DeviceSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/DeviceSerializationTest.kt @@ -53,7 +53,6 @@ class DeviceSerializationTest { bootTime = DateUtils.getDateTime("2004-11-04T08:38:00.000Z") timezone = TimeZone.getTimeZone("Europe/Vienna") id = "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196" - language = "6dd45f60-111d-42d8-9204-0452cc836ad8" connectionType = "9ceb3a6c-5292-4ed9-8665-5732495e8ed4" batteryTemperature = 0.14775127f cpuDescription = "cpu0" diff --git a/sentry/src/test/java/io/sentry/protocol/DeviceTest.kt b/sentry/src/test/java/io/sentry/protocol/DeviceTest.kt index 912f0d998f9..679bca941b2 100644 --- a/sentry/src/test/java/io/sentry/protocol/DeviceTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/DeviceTest.kt @@ -58,7 +58,6 @@ class DeviceTest { device.bootTime = Date() device.timezone = TimeZone.getDefault() device.id = "id" - device.language = "language" device.connectionType = "connection type" device.batteryTemperature = 30f device.locale = "en-US" @@ -99,7 +98,6 @@ class DeviceTest { assertEquals(1.5f, clone.screenDensity) assertEquals(300, clone.screenDpi) assertEquals("id", clone.id) - assertEquals("language", clone.language) assertEquals("connection type", clone.connectionType) assertEquals(30f, clone.batteryTemperature) assertEquals("cpu0", clone.cpuDescription) diff --git a/sentry/src/test/java/io/sentry/protocol/SentryBaseEventSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryBaseEventSerializationTest.kt index 3da517ef56f..94234c0b340 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryBaseEventSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryBaseEventSerializationTest.kt @@ -56,7 +56,7 @@ class SentryBaseEventSerializationTest { setOperatingSystem(OperatingSystemSerializationTest.Fixture().getSut()) setRuntime(SentryRuntimeSerializationTest.Fixture().getSut()) setResponse(ResponseSerializationTest.Fixture().getSut()) - trace = SpanContextSerializationTest.Fixture().getSut() + setTrace(SpanContextSerializationTest.Fixture().getSut()) } sdk = SdkVersionSerializationTest.Fixture().getSut() request = RequestSerializationTest.Fixture().getSut() diff --git a/sentry/src/test/java/io/sentry/protocol/SentryIdTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryIdTest.kt index 2aa24ec8598..a517185e1bb 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryIdTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryIdTest.kt @@ -1,5 +1,12 @@ package io.sentry.protocol +import io.sentry.SentryUUID +import io.sentry.util.StringUtils +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import java.util.* import kotlin.test.Test import kotlin.test.assertEquals @@ -10,4 +17,74 @@ class SentryIdTest { val id = SentryId("0000-0000") assertEquals("00000000000000000000000000000000", id.toString()) } + + @Test + fun `dashes are stripped if initialized with 36char uuid string`() { + val uuidString = UUID.randomUUID().toString() + val id = SentryId(uuidString) + assertEquals(uuidString.replace("-", ""), id.toString()) + } + + @Test + fun `UUID is not generated on initialization`() { + val uuid = SentryUUID.generateSentryId() + Mockito.mockStatic(SentryUUID::class.java).use { utils -> + utils.`when` { SentryUUID.generateSentryId() }.thenReturn(uuid) + val ignored = SentryId() + utils.verify({ SentryUUID.generateSentryId() }, never()) + } + } + + @Test + fun `UUID is generated only once`() { + val uuid = SentryUUID.generateSentryId() + Mockito.mockStatic(SentryUUID::class.java).use { utils -> + utils.`when` { SentryUUID.generateSentryId() }.thenReturn(uuid) + val sentryId = SentryId() + val uuid1 = sentryId.toString() + val uuid2 = sentryId.toString() + + assertEquals(uuid1, uuid2) + utils.verify({ SentryUUID.generateSentryId() }, times(1)) + } + } + + @Test + fun `normalizeUUID is never called when using empty constructor`() { + Mockito.mockStatic(StringUtils::class.java).use { utils -> + utils.`when` { StringUtils.normalizeUUID(any()) }.thenReturn("00000000000000000000000000000000") + val sentryId = SentryId() + val uuid1 = sentryId.toString() + val uuid2 = sentryId.toString() + + assertEquals(uuid1, uuid2) + utils.verify({ StringUtils.normalizeUUID(any()) }, times(0)) + } + } + + @Test + fun `normalizeUUID is only called once when String is passed to constructor`() { + Mockito.mockStatic(StringUtils::class.java).use { utils -> + utils.`when` { StringUtils.normalizeUUID(any()) }.thenReturn("00000000000000000000000000000000") + val sentryId = SentryId("00000000000000000000000000000000") + val uuid1 = sentryId.toString() + val uuid2 = sentryId.toString() + + assertEquals(uuid1, uuid2) + utils.verify({ StringUtils.normalizeUUID(any()) }, times(1)) + } + } + + @Test + fun `normalizeUUID is only called once when UUID is passed to constructor`() { + Mockito.mockStatic(StringUtils::class.java).use { utils -> + utils.`when` { StringUtils.normalizeUUID(any()) }.thenReturn("00000000000000000000000000000000") + val sentryId = SentryId(UUID.randomUUID()) + val uuid1 = sentryId.toString() + val uuid2 = sentryId.toString() + + assertEquals(uuid1, uuid2) + utils.verify({ StringUtils.normalizeUUID(any()) }, times(1)) + } + } } diff --git a/sentry/src/test/java/io/sentry/protocol/SentryItemTypeSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryItemTypeSerializationTest.kt index c50a12b80de..6e7a8191b84 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryItemTypeSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryItemTypeSerializationTest.kt @@ -30,7 +30,6 @@ class SentryItemTypeSerializationTest { assertEquals(serialize(SentryItemType.ReplayRecording), json("replay_recording")) assertEquals(serialize(SentryItemType.ReplayVideo), json("replay_video")) assertEquals(serialize(SentryItemType.CheckIn), json("check_in")) - assertEquals(serialize(SentryItemType.Statsd), json("statsd")) assertEquals(serialize(SentryItemType.Feedback), json("feedback")) } @@ -47,7 +46,6 @@ class SentryItemTypeSerializationTest { assertEquals(deserialize(json("replay_recording")), SentryItemType.ReplayRecording) assertEquals(deserialize(json("replay_video")), SentryItemType.ReplayVideo) assertEquals(deserialize(json("check_in")), SentryItemType.CheckIn) - assertEquals(deserialize(json("statsd")), SentryItemType.Statsd) assertEquals(deserialize(json("feedback")), SentryItemType.Feedback) } diff --git a/sentry/src/test/java/io/sentry/protocol/SentrySpanSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentrySpanSerializationTest.kt index b9d4d347929..1612a993d49 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentrySpanSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentrySpanSerializationTest.kt @@ -30,17 +30,6 @@ class SentrySpanSerializationTest { "auto.test.unit.span", mapOf("f1333f3a-916a-47b7-8dd6-d6d15fa96e03" to "d4a07684-5b3e-4d08-b605-f9364c398124"), mapOf("test_measurement" to MeasurementValue(1, "test")), - mapOf( - "d:custom/background_operation@second" to listOf( - MetricSummary( - 1.0, - 2.0, - 3.0, - 2, - mapOf("environment" to "production") - ) - ) - ), mapOf("518276a7-88d7-408f-ab36-af342f2d7715" to "4a1c2d6c-3f49-41cc-b2ca-d1b36f7ea5a6") ) } diff --git a/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt index 27499be0a0c..504f5bcccc7 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt @@ -1,6 +1,6 @@ package io.sentry.protocol -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.SentryLongDate import io.sentry.SentryTracer import io.sentry.Span @@ -19,9 +19,8 @@ class SentrySpanTest { val span = Span( TransactionContext("name", "op"), mock(), - mock(), - SentryLongDate(1000000), - SpanOptions() + mock(), + SpanOptions().also { it.startTimestamp = SentryLongDate(1000000) } ) val sentrySpan = SentrySpan(span) diff --git a/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt index 120bda922c2..cf9463191a1 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryTransactionSerializationTest.kt @@ -31,17 +31,6 @@ class SentryTransactionSerializationTest { "386384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(), "186384cb-1162-49e7-aea1-db913d4fca63" to MeasurementValueSerializationTest.Fixture().getSut(0.4000000059604645, "test2") ), - mapOf( - "d:custom/background_operation@second" to listOf( - MetricSummary( - 5.0, - 6.0, - 11.0, - 3, - mapOf("environment" to "production") - ) - ) - ), TransactionInfo(TransactionNameSource.CUSTOM.apiName()) ).apply { SentryBaseEventSerializationTest.Fixture().update(this) diff --git a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt index f8ca3719c72..129e763331f 100644 --- a/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SessionSerializationTest.kt @@ -11,7 +11,6 @@ import org.junit.Test import org.mockito.kotlin.mock import java.io.StringReader import java.io.StringWriter -import java.util.UUID import kotlin.test.assertEquals class SessionSerializationTest { @@ -25,7 +24,7 @@ class SessionSerializationTest { DateUtils.getDateTime("1970-04-21T09:32:21.000Z"), 9001, "631693c2-3d61-4a93-8fd1-89817426ba5a", - UUID.fromString("3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17"), + "3c1ffc32-f68f-4af2-a1ee-dd72f4d62d17", true, 4, 5.5, diff --git a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt index bd3bcd72c35..707daa78f10 100644 --- a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt @@ -35,6 +35,7 @@ class SpanContextSerializationTest { setTag("2a5fa3f5-7b87-487f-aaa5-84567aa73642", "4781d51a-c5af-47f2-a4ed-f030c9b3e194") setTag("29106d7d-7fa4-444f-9d34-b9d7510c69ab", "218c23ea-694a-497e-bf6d-e5f26f1ad7bd") setTag("ba9ce913-269f-4c03-882d-8ca5e6991b14", "35a74e90-8db8-4610-a411-872cbc1030ac") + setData("spanContextDataKey", "spanContextDataValue") } } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/protocol/SpanIdTest.kt b/sentry/src/test/java/io/sentry/protocol/SpanIdTest.kt new file mode 100644 index 00000000000..f21a9d59309 --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SpanIdTest.kt @@ -0,0 +1,36 @@ +package io.sentry.protocol + +import io.sentry.SentryUUID +import io.sentry.SpanId +import org.mockito.Mockito +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import kotlin.test.Test +import kotlin.test.assertEquals + +class SpanIdTest { + + @Test + fun `ID is not generated on initialization`() { + val uuid = SentryUUID.generateSpanId() + Mockito.mockStatic(SentryUUID::class.java).use { utils -> + utils.`when` { SentryUUID.generateSpanId() }.thenReturn(uuid) + val ignored = SpanId() + utils.verify({ SentryUUID.generateSpanId() }, never()) + } + } + + @Test + fun `ID is generated only once`() { + val uuid = SentryUUID.generateSpanId() + Mockito.mockStatic(SentryUUID::class.java).use { utils -> + utils.`when` { SentryUUID.generateSpanId() }.thenReturn(uuid) + val spanId = SpanId() + val uuid1 = spanId.toString() + val uuid2 = spanId.toString() + + assertEquals(uuid1, uuid2) + utils.verify({ SentryUUID.generateSpanId() }, times(1)) + } + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/UserSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/UserSerializationTest.kt index 55c3648f437..cbe44e70f3a 100644 --- a/sentry/src/test/java/io/sentry/protocol/UserSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/UserSerializationTest.kt @@ -29,7 +29,7 @@ class UserSerializationTest { countryCode = "JP" region = "273a3d0a-b1c5-11ed-afa1-0242ac120002" } - others = mapOf( + data = mapOf( "dc2813d0-0f66-4a3f-a995-71268f61a8fa" to "991659ad-7c59-4dd3-bb89-0bd5c74014bd" ) } @@ -51,23 +51,6 @@ class UserSerializationTest { assertEquals(expectedJson, actualJson) } - @Test - fun `deserialize legacy`() { - var expectedJson = sanitizedFile("json/user.json") - val expected = deserialize(expectedJson) - - // Not part of this test - expected.name = null - expected.geo = null - - expectedJson = serialize(expected) - - val inputJson = sanitizedFile("json/user_legacy.json") - val actual = deserialize(inputJson) - val actualJson = serialize(actual) - assertEquals(expectedJson, actualJson) - } - @Test fun deserializeFromMap() { val map: Map = mapOf( diff --git a/sentry/src/test/java/io/sentry/protocol/UserTest.kt b/sentry/src/test/java/io/sentry/protocol/UserTest.kt index 2a146f83a8d..b6758a4650b 100644 --- a/sentry/src/test/java/io/sentry/protocol/UserTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/UserTest.kt @@ -31,7 +31,6 @@ class UserTest { assertEquals("123", clone.id) assertEquals("123.x", clone.ipAddress) assertEquals("userName", clone.username) - assertEquals("userSegment", clone.segment) assertEquals("data", clone.data!!["data"]) assertEquals("unknown", clone.unknown!!["unknown"]) } @@ -45,7 +44,6 @@ class UserTest { user.id = "456" user.ipAddress = "456.x" user.username = "newUserName" - user.segment = "newUserSegment" user.data!!["data"] = "newOthers" user.data!!["anotherOne"] = "anotherOne" val newUnknown = mapOf(Pair("unknown", "newUnknown"), Pair("otherUnknown", "otherUnknown")) @@ -55,7 +53,6 @@ class UserTest { assertEquals("123", clone.id) assertEquals("123.x", clone.ipAddress) assertEquals("userName", clone.username) - assertEquals("userSegment", clone.segment) assertEquals("data", clone.data!!["data"]) assertEquals(1, clone.data!!.size) assertEquals("unknown", clone.unknown!!["unknown"]) @@ -87,7 +84,6 @@ class UserTest { id = "123" ipAddress = "123.x" username = "userName" - segment = "userSegment" val data = mutableMapOf(Pair("data", "data")) setData(data) val unknown = mapOf(Pair("unknown", "unknown")) diff --git a/sentry/src/test/java/io/sentry/rrweb/RRWebOptionsEventSerializationTest.kt b/sentry/src/test/java/io/sentry/rrweb/RRWebOptionsEventSerializationTest.kt new file mode 100644 index 00000000000..c1ca71b6fdc --- /dev/null +++ b/sentry/src/test/java/io/sentry/rrweb/RRWebOptionsEventSerializationTest.kt @@ -0,0 +1,47 @@ +package io.sentry.rrweb + +import io.sentry.ILogger +import io.sentry.SentryOptions +import io.sentry.SentryReplayOptions.SentryReplayQuality.LOW +import io.sentry.protocol.SdkVersion +import io.sentry.protocol.SerializationUtils +import org.junit.Test +import org.mockito.kotlin.mock +import kotlin.test.assertEquals + +class RRWebOptionsEventSerializationTest { + class Fixture { + val logger = mock() + + fun getSut() = RRWebOptionsEvent( + SentryOptions().apply { + sdkVersion = SdkVersion("sentry.java", "7.19.1") + sessionReplay.sessionSampleRate = 0.5 + sessionReplay.onErrorSampleRate = 0.1 + sessionReplay.quality = LOW + sessionReplay.unmaskViewClasses.add("com.example.MyClass") + sessionReplay.maskViewClasses.clear() + } + ).apply { + timestamp = 12345678901 + } + } + + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = SerializationUtils.sanitizedFile("json/rrweb_options_event.json") + val actual = SerializationUtils.serializeToString(fixture.getSut(), fixture.logger) + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = SerializationUtils.sanitizedFile("json/rrweb_options_event.json") + val actual = + SerializationUtils.deserializeJson(expectedJson, RRWebOptionsEvent.Deserializer(), fixture.logger) + val actualJson = SerializationUtils.serializeToString(actual, fixture.logger) + assertEquals(expectedJson, actualJson) + } +} diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 8d8fb9601ef..e9a38631cce 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -5,8 +5,8 @@ import io.sentry.CheckIn import io.sentry.CheckInStatus import io.sentry.DataCategory.Replay import io.sentry.Hint -import io.sentry.IHub import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.ISerializer import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData @@ -24,10 +24,11 @@ import io.sentry.TransactionContext import io.sentry.UserFeedback import io.sentry.clientreport.DiscardReason import io.sentry.clientreport.IClientReportRecorder -import io.sentry.metrics.EncodedMetrics +import io.sentry.hints.DiskFlushNotification import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.util.HintUtils import org.awaitility.kotlin.await import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -87,10 +88,10 @@ class RateLimiterTest { fun `parse X-Sentry-Rate-Limit and set its values and retry after should be true`() { val rateLimiter = fixture.getSUT() whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0) - val hub: IHub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes: IScopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) - val transaction = SentryTransaction(SentryTracer(TransactionContext("name", "op"), hub)) + val transaction = SentryTransaction(SentryTracer(TransactionContext("name", "op"), scopes)) val transactionItem = SentryEnvelopeItem.fromEvent(fixture.serializer, transaction) val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, transactionItem)) @@ -104,19 +105,18 @@ class RateLimiterTest { fun `parse X-Sentry-Rate-Limit and set its values and retry after should be false`() { val rateLimiter = fixture.getSUT() whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 0, 1001) - val hub: IHub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes: IScopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) - val transaction = SentryTransaction(SentryTracer(TransactionContext("name", "op"), hub)) + val transaction = SentryTransaction(SentryTracer(TransactionContext("name", "op"), scopes)) val transactionItem = SentryEnvelopeItem.fromEvent(fixture.serializer, transaction) - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, transactionItem, statsdItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, transactionItem)) rateLimiter.updateRetryAfterLimits("1:transaction:key, 1:default;error;metric_bucket;security:organization", null, 1) val result = rateLimiter.filter(envelope, Hint()) assertNotNull(result) - assertEquals(3, result.items.count()) + assertEquals(2, result.items.count()) } @Test @@ -199,17 +199,16 @@ class RateLimiterTest { it.setName("John Me") } ) - val hub = mock() - whenever(hub.options).thenReturn(SentryOptions()) - val transaction = SentryTracer(TransactionContext("name", "op"), hub) + val scopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) + val transaction = SentryTracer(TransactionContext("name", "op"), scopes) val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release")) val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val checkInItem = SentryEnvelopeItem.fromCheckIn(fixture.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, statsdItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem)) rateLimiter.updateRetryAfterLimits(null, null, 429) val result = rateLimiter.filter(envelope, Hint()) @@ -222,15 +221,14 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(checkInItem)) - verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } @Test fun `records only dropped items as lost`() { val rateLimiter = fixture.getSUT() - val hub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) val userFeedbackItem = SentryEnvelopeItem.fromUserFeedback( @@ -243,7 +241,7 @@ class RateLimiterTest { it.setName("John Me") } ) - val transaction = SentryTracer(TransactionContext("name", "op"), hub) + val transaction = SentryTracer(TransactionContext("name", "op"), scopes) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release")) val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) @@ -263,12 +261,12 @@ class RateLimiterTest { @Test fun `drop profile items as lost`() { val rateLimiter = fixture.getSUT() - val hub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) val f = File.createTempFile("test", "trace") - val transaction = SentryTracer(TransactionContext("name", "op"), hub) + val transaction = SentryTracer(TransactionContext("name", "op"), scopes) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(f, transaction), 1000, fixture.serializer) val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, profileItem)) @@ -283,89 +281,40 @@ class RateLimiterTest { } @Test - fun `drop metrics items as lost`() { + fun `any limit can be checked`() { val rateLimiter = fixture.getSUT() - val hub = mock() - whenever(hub.options).thenReturn(SentryOptions()) - + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0) val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) - val f = File.createTempFile("test", "trace") - val transaction = SentryTracer(TransactionContext("name", "op"), hub) - val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(f, transaction), 1000, fixture.serializer) - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, profileItem, statsdItem)) - - rateLimiter.updateRetryAfterLimits("60:metric_bucket:key", null, 1) - val result = rateLimiter.filter(envelope, Hint()) - - assertNotNull(result) - assertEquals(2, result.items.toList().size) - - verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) - verifyNoMoreInteractions(fixture.clientReportRecorder) - } - - @Test - fun `drop metrics items if namespace is custom`() { - val rateLimiter = fixture.getSUT() - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) - - rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded:custom", null, 1) - val result = rateLimiter.filter(envelope, Hint()) - assertNull(result) - - verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) - verifyNoMoreInteractions(fixture.clientReportRecorder) - } - - @Test - fun `drop metrics items if namespaces is empty`() { - val rateLimiter = fixture.getSUT() - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) - - rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded::", null, 1) - val result = rateLimiter.filter(envelope, Hint()) - assertNull(result) - - verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) - verifyNoMoreInteractions(fixture.clientReportRecorder) - } + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem)) - @Test - fun `drop metrics items if namespaces is not present`() { - val rateLimiter = fixture.getSUT() - val statsdItem = SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(statsdItem)) + assertFalse(rateLimiter.isAnyRateLimitActive) - rateLimiter.updateRetryAfterLimits("60:metric_bucket:key:quota_exceeded", null, 1) - val result = rateLimiter.filter(envelope, Hint()) - assertNull(result) + rateLimiter.updateRetryAfterLimits("50:transaction:key, 1:default;error;security:organization", null, 1) - verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(statsdItem)) - verifyNoMoreInteractions(fixture.clientReportRecorder) + assertTrue(rateLimiter.isAnyRateLimitActive) } @Test - fun `any limit can be checked`() { + fun `on rate limit DiskFlushNotification is marked as flushed`() { val rateLimiter = fixture.getSUT() whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0) - val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem)) - - assertFalse(rateLimiter.isAnyRateLimitActive) + val sentryEvent = SentryEvent() + val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, sentryEvent) + val envelope = SentryEnvelope(SentryEnvelopeHeader(sentryEvent.eventId), arrayListOf(eventItem)) rateLimiter.updateRetryAfterLimits("50:transaction:key, 1:default;error;security:organization", null, 1) - assertTrue(rateLimiter.isAnyRateLimitActive) + val hint = mock() + rateLimiter.filter(envelope, HintUtils.createWithTypeCheckHint(hint)) + + verify(hint).markFlushed() } @Test fun `drop replay items as lost`() { val rateLimiter = fixture.getSUT() - val hub = mock() - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + whenever(scopes.options).thenReturn(SentryOptions()) val replayItem = SentryEnvelopeItem.fromReplay(fixture.serializer, mock(), SentryReplayEvent(), ReplayRecording(), false) val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) @@ -422,8 +371,10 @@ class RateLimiterTest { rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) rateLimiter.close() - // wait for 1.5s to ensure the timer has run after 1s - await.untilTrue(applied) + // If rate limit didn't already change, wait for 1.5s to ensure the timer has run after 1s + if (!applied.get()) { + await.untilTrue(applied) + } assertTrue(applied.get()) } } diff --git a/sentry/src/test/java/io/sentry/util/AutoClosableReentrantLockTest.kt b/sentry/src/test/java/io/sentry/util/AutoClosableReentrantLockTest.kt new file mode 100644 index 00000000000..1f3c853c6a2 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/AutoClosableReentrantLockTest.kt @@ -0,0 +1,17 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class AutoClosableReentrantLockTest { + + @Test + fun `calls lock in acquire and unlock on close`() { + val lock = AutoClosableReentrantLock() + lock.acquire().use { + assertTrue(lock.isLocked) + } + assertFalse(lock.isLocked) + } +} diff --git a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt index a285cd58326..cc15da93c31 100644 --- a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt @@ -1,7 +1,9 @@ package io.sentry.util import io.sentry.CheckInStatus -import io.sentry.IHub +import io.sentry.FilterString +import io.sentry.IScopes +import io.sentry.ISentryLifecycleToken import io.sentry.MonitorConfig import io.sentry.MonitorSchedule import io.sentry.MonitorScheduleUnit @@ -25,12 +27,12 @@ class CheckInUtilsTest { @Test fun `ignores exact match`() { - assertTrue(CheckInUtils.isIgnored(listOf("slugA"), "slugA")) + assertTrue(CheckInUtils.isIgnored(listOf(FilterString("slugA")), "slugA")) } @Test fun `ignores regex match`() { - assertTrue(CheckInUtils.isIgnored(listOf("slug-.*"), "slug-A")) + assertTrue(CheckInUtils.isIgnored(listOf(FilterString("slug-.*")), "slug-A")) } @Test @@ -45,41 +47,88 @@ class CheckInUtilsTest { @Test fun `does not ignore if slug is not in ignored list`() { - assertFalse(CheckInUtils.isIgnored(listOf("slugB"), "slugA")) + assertFalse(CheckInUtils.isIgnored(listOf(FilterString("slugB")), "slugA")) } @Test fun `does not ignore if slug is does not match ignored list`() { - assertFalse(CheckInUtils.isIgnored(listOf("slug-.*"), "slugA")) + assertFalse(CheckInUtils.isIgnored(listOf(FilterString("slug-.*")), "slugA")) } @Test fun `sends check-in for wrapped supplier`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + val lifecycleToken = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + sentry.`when` { Sentry.forkedScopes(any()) }.then { + scopes.forkedScopes("test") + } + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + whenever(scopes.options).thenReturn(SentryOptions()) val returnValue = CheckInUtils.withCheckIn("monitor-1") { return@withCheckIn "test1" } assertEquals("test1", returnValue) - inOrder(hub) { - verify(hub).pushScope() - verify(hub).configureScope(any()) - verify(hub).captureCheckIn( + inOrder(scopes, lifecycleToken) { + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + verify(scopes).configureScope(any()) + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.IN_PROGRESS.apiName(), it.status) } ) - verify(hub).captureCheckIn( + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), it.status) } ) - verify(hub).popScope() + verify(lifecycleToken).close() + } + } + } + + @Test + fun `sends check-in for wrapped supplier with environment`() { + Mockito.mockStatic(Sentry::class.java).use { sentry -> + val scopes = mock() + val lifecycleToken = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + sentry.`when` { Sentry.forkedScopes(any()) }.then { + scopes.forkedScopes("test") + } + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + whenever(scopes.options).thenReturn(SentryOptions()) + val returnValue = CheckInUtils.withCheckIn("monitor-1", "environment-1") { + return@withCheckIn "test1" + } + + assertEquals("test1", returnValue) + inOrder(scopes, lifecycleToken) { + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + verify(scopes).configureScope(any()) + verify(scopes).captureCheckIn( + check { + assertEquals("monitor-1", it.monitorSlug) + assertEquals("environment-1", it.environment) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), it.status) + } + ) + verify(scopes).captureCheckIn( + check { + assertEquals("monitor-1", it.monitorSlug) + assertEquals("environment-1", it.environment) + assertEquals(CheckInStatus.OK.apiName(), it.status) + } + ) + verify(lifecycleToken).close() } } } @@ -87,8 +136,14 @@ class CheckInUtilsTest { @Test fun `sends check-in for wrapped supplier with exception`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + val scopes = mock() + val lifecycleToken = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + sentry.`when` { Sentry.forkedScopes(any()) }.then { + scopes.forkedScopes("test") + } + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) try { CheckInUtils.withCheckIn("monitor-1") { @@ -99,22 +154,23 @@ class CheckInUtilsTest { assertEquals("thrown on purpose", e.message) } - inOrder(hub) { - verify(hub).pushScope() - verify(hub).configureScope(any()) - verify(hub).captureCheckIn( + inOrder(scopes, lifecycleToken) { + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + verify(scopes).configureScope(any()) + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.IN_PROGRESS.apiName(), it.status) } ) - verify(hub).captureCheckIn( + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.ERROR.apiName(), it.status) } ) - verify(hub).popScope() + verify(lifecycleToken).close() } } } @@ -122,32 +178,39 @@ class CheckInUtilsTest { @Test fun `sends check-in for wrapped supplier with upsert`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + val lifecycleToken = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + sentry.`when` { Sentry.forkedScopes(any()) }.then { + scopes.forkedScopes("test") + } + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + whenever(scopes.options).thenReturn(SentryOptions()) val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)) val returnValue = CheckInUtils.withCheckIn("monitor-1", monitorConfig) { "test1" } assertEquals("test1", returnValue) - inOrder(hub) { - verify(hub).pushScope() - verify(hub).configureScope(any()) - verify(hub).captureCheckIn( + inOrder(scopes, lifecycleToken) { + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + verify(scopes).configureScope(any()) + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertSame(monitorConfig, it.monitorConfig) assertEquals(CheckInStatus.IN_PROGRESS.apiName(), it.status) } ) - verify(hub).captureCheckIn( + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), it.status) } ) - verify(hub).popScope() + verify(lifecycleToken).close() } } } @@ -155,9 +218,15 @@ class CheckInUtilsTest { @Test fun `sends check-in for wrapped supplier with upsert and thresholds`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) - whenever(hub.options).thenReturn(SentryOptions()) + val scopes = mock() + val lifecycleToken = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + sentry.`when` { Sentry.forkedScopes(any()) }.then { + scopes.forkedScopes("test") + } + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + whenever(scopes.options).thenReturn(SentryOptions()) val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)).apply { failureIssueThreshold = 10 recoveryThreshold = 20 @@ -167,23 +236,24 @@ class CheckInUtilsTest { } assertEquals("test1", returnValue) - inOrder(hub) { - verify(hub).pushScope() - verify(hub).configureScope(any()) - verify(hub).captureCheckIn( + inOrder(scopes, lifecycleToken) { + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + verify(scopes).configureScope(any()) + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertSame(monitorConfig, it.monitorConfig) assertEquals(CheckInStatus.IN_PROGRESS.apiName(), it.status) } ) - verify(hub).captureCheckIn( + verify(scopes).captureCheckIn( check { assertEquals("monitor-1", it.monitorSlug) assertEquals(CheckInStatus.OK.apiName(), it.status) } ) - verify(hub).popScope() + verify(lifecycleToken).close() } } } @@ -191,9 +261,9 @@ class CheckInUtilsTest { @Test fun `sets defaults for MonitorConfig from SentryOptions`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) - whenever(hub.options).thenReturn( + val scopes = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + whenever(scopes.options).thenReturn( SentryOptions().apply { cron = SentryOptions.Cron().apply { defaultCheckinMargin = 20 @@ -218,9 +288,9 @@ class CheckInUtilsTest { @Test fun `defaults for MonitorConfig from SentryOptions can be overridden`() { Mockito.mockStatic(Sentry::class.java).use { sentry -> - val hub = mock() - sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) - whenever(hub.options).thenReturn( + val scopes = mock() + sentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes) + whenever(scopes.options).thenReturn( SentryOptions().apply { cron = SentryOptions.Cron().apply { defaultCheckinMargin = 20 diff --git a/sentry/src/test/java/io/sentry/util/InitUtilTest.kt b/sentry/src/test/java/io/sentry/util/InitUtilTest.kt new file mode 100644 index 00000000000..c3c73f61671 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/InitUtilTest.kt @@ -0,0 +1,107 @@ +package io.sentry.util + +import io.sentry.InitPriority +import io.sentry.SentryOptions +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class InitUtilTest { + + private var previousOptions: SentryOptions? = null + private var newOptions: SentryOptions? = null + private var clientEnabled: Boolean = true + + @BeforeTest + fun setup() { + previousOptions = null + newOptions = null + clientEnabled = true + } + + @Test + fun `first init on empty options goes through`() { + givenPreviousOptions(SentryOptions.empty()) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientDisabled() + + thenInitIsPerformed() + } + + @Test + fun `init with same priority goes through`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsPerformed() + } + + @Test + fun `init without previous options goes through`() { + givenPreviousOptions(null) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsPerformed() + } + + @Test + fun `init with lower priority is ignored if already initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsIgnored() + } + + @Test + fun `init with lower priority goes through if not yet initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientDisabled() + + thenInitIsPerformed() + } + + @Test + fun `init with lower priority goes through with forceInit if already initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions( + SentryOptions().also { + it.initPriority = InitPriority.LOWEST + it.isForceInit = true + } + ) + givenClientEnabled() + + thenInitIsPerformed() + } + + private fun givenPreviousOptions(options: SentryOptions?) { + previousOptions = options + } + + private fun givenNewOptions(options: SentryOptions?) { + newOptions = options + } + + private fun givenClientDisabled() { + clientEnabled = false + } + + private fun givenClientEnabled() { + clientEnabled = true + } + + private fun thenInitIsPerformed() { + val shouldInit = InitUtil.shouldInit(previousOptions, newOptions!!, clientEnabled) + assertTrue(shouldInit) + } + + private fun thenInitIsIgnored() { + val shouldInit = InitUtil.shouldInit(previousOptions, newOptions!!, clientEnabled) + assertFalse(shouldInit) + } +} diff --git a/sentry/src/test/java/io/sentry/util/MapObjectReaderTest.kt b/sentry/src/test/java/io/sentry/util/MapObjectReaderTest.kt index a335fc71f82..c27269c9dfc 100644 --- a/sentry/src/test/java/io/sentry/util/MapObjectReaderTest.kt +++ b/sentry/src/test/java/io/sentry/util/MapObjectReaderTest.kt @@ -13,7 +13,6 @@ import java.util.Currency import java.util.Date import java.util.Locale import java.util.TimeZone -import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals @@ -71,7 +70,7 @@ class MapObjectReaderTest { writer.name("MapOfLists").value(logger, mapOf("metric_a" to listOf("foo"))) writer.name("Locale").value(logger, Locale.US) writer.name("URI").value(logger, URI.create("http://www.example.com")) - writer.name("UUID").value(logger, UUID.fromString("00000000-1111-2222-3333-444444444444")) + writer.name("UUID").value(logger, "00000000-1111-2222-3333-444444444444") writer.name("Currency").value(logger, Currency.getInstance("EUR")) writer.name("Enum").value(logger, MapObjectWriterTest.BasicEnum.A) writer.name("data").value(logger, mapOf("screen" to "MainActivity")) @@ -96,8 +95,8 @@ class MapObjectReaderTest { assertEquals(Currency.getInstance("EUR"), Currency.getInstance(reader.nextString())) assertEquals("UUID", reader.nextName()) assertEquals( - UUID.fromString("00000000-1111-2222-3333-444444444444"), - UUID.fromString(reader.nextString()) + "00000000-1111-2222-3333-444444444444", + reader.nextString() ) assertEquals("URI", reader.nextName()) assertEquals(URI.create("http://www.example.com"), URI.create(reader.nextString())) diff --git a/sentry/src/test/java/io/sentry/util/MapObjectWriterTest.kt b/sentry/src/test/java/io/sentry/util/MapObjectWriterTest.kt index 4127a8c840f..d52ae8dbc68 100644 --- a/sentry/src/test/java/io/sentry/util/MapObjectWriterTest.kt +++ b/sentry/src/test/java/io/sentry/util/MapObjectWriterTest.kt @@ -12,7 +12,6 @@ import java.util.Currency import java.util.Date import java.util.Locale import java.util.TimeZone -import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicIntegerArray import kotlin.test.Test @@ -61,7 +60,7 @@ class MapObjectWriterTest { writer.name("AtomicBoolean").value(logger, AtomicBoolean(false)) writer.name("URI").value(logger, URI.create("http://www.example.com")) writer.name("InetAddress").value(logger, Inet4Address.getByName("1.1.1.1")) - writer.name("UUID").value(logger, UUID.fromString("00000000-1111-2222-3333-444444444444")) + writer.name("UUID").value(logger, "00000000-1111-2222-3333-444444444444") writer.name("Currency").value(logger, Currency.getInstance("EUR")) writer.name("Calendar").value( logger, diff --git a/sentry/src/test/java/io/sentry/util/SpanUtilsTest.kt b/sentry/src/test/java/io/sentry/util/SpanUtilsTest.kt new file mode 100644 index 00000000000..90f721432c3 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/SpanUtilsTest.kt @@ -0,0 +1,81 @@ +package io.sentry.util + +import io.sentry.FilterString +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SpanUtilsTest { + + @Test + fun `isIgnored returns true for exact match`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns true for exact match with multiple invocations`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns true for regex match`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring.*")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns true for regex match with multiple invocations`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring.*")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + assertTrue(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns false for no match`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring.webflux")) + } + + @Test + fun `isIgnored returns false for no match with multiple invocations`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(ignoredOrigins, "auto.http.spring.webflux")) + } + + @Test + fun `isIgnored returns false for null origin`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(ignoredOrigins, null)) + } + + @Test + fun `isIgnored returns false for null origin with multiple invocations`() { + val ignoredOrigins = listOf(FilterString("auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(ignoredOrigins, null)) + } + + @Test + fun `isIgnored returns false for null ignoredOrigins`() { + assertFalse(SpanUtils.isIgnored(null, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns false for null ignoredOrigins with multiple invocations`() { + assertFalse(SpanUtils.isIgnored(null, "auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(null, "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns false for empty ignoredOrigins`() { + assertFalse(SpanUtils.isIgnored(emptyList(), "auto.http.spring_jakarta.webmvc")) + } + + @Test + fun `isIgnored returns false for empty ignoredOrigins with multiple invocations`() { + assertFalse(SpanUtils.isIgnored(emptyList(), "auto.http.spring_jakarta.webmvc")) + assertFalse(SpanUtils.isIgnored(emptyList(), "auto.http.spring_jakarta.webmvc")) + } +} diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index e38410641e4..e5403f54d99 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -1,7 +1,7 @@ package io.sentry.util import io.sentry.Baggage -import io.sentry.IHub +import io.sentry.IScopes import io.sentry.NoOpSpan import io.sentry.Scope import io.sentry.ScopeCallback @@ -29,19 +29,18 @@ class TracingUtilsTest { val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } - val hub = mock() + val scopes = mock() val scope = Scope(options) lateinit var span: Span val preExistingBaggage = listOf("some-baggage-key=some-baggage-value") fun setup() { - whenever(hub.options).thenReturn(options) - doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(hub).configureScope(any()) + whenever(scopes.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(scopes).configureScope(any()) span = Span( TransactionContext("name", "op", TracesSamplingDecision(true)), - SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), hub), - hub, - null, + SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), scopes), + scopes, SpanOptions() ) } @@ -53,12 +52,13 @@ class TracingUtilsTest { fun `returns headers if allowed from scope without span`() { fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, null) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, null) assertNotNull(headers) assertNotNull(headers.baggageHeader) assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId) assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId) + assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled) assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value")) assertTrue(headers.baggageHeader!!.value.contains("sentry-trace_id=${fixture.scope.propagationContext.traceId}")) assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) @@ -68,12 +68,61 @@ class TracingUtilsTest { fun `returns headers if allowed from scope if span is noop`() { fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance()) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance()) assertNotNull(headers) assertNotNull(headers.baggageHeader) assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId) assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId) + assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled) + assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value")) + assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) + } + + @Test + fun `returns headers if allowed from scope if span is noop sampled=null`() { + fixture.setup() + fixture.scope.propagationContext.isSampled = null + + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance()) + + assertNotNull(headers) + assertNotNull(headers.baggageHeader) + assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId) + assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId) + assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled) + assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value")) + assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) + } + + @Test + fun `returns headers if allowed from scope if span is noop sampled=true`() { + fixture.setup() + fixture.scope.propagationContext.isSampled = true + + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance()) + + assertNotNull(headers) + assertNotNull(headers.baggageHeader) + assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId) + assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId) + assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled) + assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value")) + assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) + } + + @Test + fun `returns headers if allowed from scope if span is noop sampled=false`() { + fixture.setup() + fixture.scope.propagationContext.isSampled = false + + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, NoOpSpan.getInstance()) + + assertNotNull(headers) + assertNotNull(headers.baggageHeader) + assertEquals(fixture.scope.propagationContext.spanId, headers.sentryTraceHeader.spanId) + assertEquals(fixture.scope.propagationContext.traceId, headers.sentryTraceHeader.traceId) + assertEquals(fixture.scope.propagationContext.isSampled, headers.sentryTraceHeader.isSampled) assertTrue(headers.baggageHeader!!.value.contains("some-baggage-key=some-baggage-value")) assertFalse(fixture.scope.propagationContext.baggage!!.isMutable) } @@ -83,7 +132,7 @@ class TracingUtilsTest { fixture.scope.propagationContext.baggage = Baggage.fromHeader("sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET").also { it.freeze() } fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, null) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, null) assertNotNull(headers) assertNotNull(headers.baggageHeader) @@ -98,7 +147,7 @@ class TracingUtilsTest { fun `returns headers if allowed from span`() { fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) assertNotNull(headers) assertNotNull(headers.baggageHeader) @@ -112,7 +161,7 @@ class TracingUtilsTest { fixture.options.isTraceSampling = false fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, null) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, null) assertNull(headers) } @@ -122,7 +171,7 @@ class TracingUtilsTest { fixture.options.isTraceSampling = false fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) assertNull(headers) } @@ -132,7 +181,7 @@ class TracingUtilsTest { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, null) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, null) assertNull(headers) } @@ -142,7 +191,7 @@ class TracingUtilsTest { fixture.options.setTracePropagationTargets(listOf("some-host-that-does-not-exist")) fixture.setup() - val headers = TracingUtils.traceIfAllowed(fixture.hub, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) + val headers = TracingUtils.traceIfAllowed(fixture.scopes, "https://sentry.io/hello", fixture.preExistingBaggage, fixture.span) assertNull(headers) } @@ -153,7 +202,7 @@ class TracingUtilsTest { val propagationContextBefore = fixture.scope.propagationContext - TracingUtils.startNewTrace(fixture.hub) + TracingUtils.startNewTrace(fixture.scopes) assertNotEquals(propagationContextBefore.traceId, fixture.scope.propagationContext.traceId) assertNotEquals(propagationContextBefore.spanId, fixture.scope.propagationContext.spanId) diff --git a/sentry/src/test/java/io/sentry/util/thread/MainThreadCheckerTest.kt b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt similarity index 71% rename from sentry/src/test/java/io/sentry/util/thread/MainThreadCheckerTest.kt rename to sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt index a52f56f52b0..26de021fbdc 100644 --- a/sentry/src/test/java/io/sentry/util/thread/MainThreadCheckerTest.kt +++ b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt @@ -5,25 +5,25 @@ import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue -class MainThreadCheckerTest { +class ThreadCheckerTest { - private val mainThreadChecker = MainThreadChecker.getInstance() + private val threadChecker = ThreadChecker.getInstance() @Test fun `When calling isMainThread from the same thread, it should return true`() { - assertTrue(mainThreadChecker.isMainThread) + assertTrue(threadChecker.isMainThread) } @Test fun `When calling isMainThread with the current thread, it should return true`() { val thread = Thread.currentThread() - assertTrue(mainThreadChecker.isMainThread(thread)) + assertTrue(threadChecker.isMainThread(thread)) } @Test fun `When calling isMainThread from a different thread, it should return false`() { val thread = Thread() - assertFalse(mainThreadChecker.isMainThread(thread)) + assertFalse(threadChecker.isMainThread(thread)) } @Test @@ -32,7 +32,7 @@ class MainThreadCheckerTest { val sentryThread = SentryThread().apply { id = thread.id } - assertTrue(mainThreadChecker.isMainThread(sentryThread)) + assertTrue(threadChecker.isMainThread(sentryThread)) } @Test @@ -41,6 +41,6 @@ class MainThreadCheckerTest { val sentryThread = SentryThread().apply { id = thread.id } - assertFalse(mainThreadChecker.isMainThread(sentryThread)) + assertFalse(threadChecker.isMainThread(sentryThread)) } } diff --git a/sentry/src/test/resources/envelope-transaction-with-sample-rate.txt b/sentry/src/test/resources/envelope-transaction-with-sample-rate.txt index b790648e064..172b0783776 100644 --- a/sentry/src/test/resources/envelope-transaction-with-sample-rate.txt +++ b/sentry/src/test/resources/envelope-transaction-with-sample-rate.txt @@ -1,3 +1,3 @@ -{"event_id":"3367f5196c494acaae85bbbd535379ac","trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","public_key":"key","release":"1.0-beta.1","environment":"prod","user_id":"usr1","user_segment":"pro","transaction":"tx1","sample_rate":"0.00000021"}} +{"event_id":"3367f5196c494acaae85bbbd535379ac","trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","public_key":"key","release":"1.0-beta.1","environment":"prod","user_id":"usr1","transaction":"tx1","sample_rate":"0.00000021"}} {"type":"transaction","length":640,"content_type":"application/json"} {"transaction":"a-transaction","type":"transaction","start_timestamp":"2020-10-23T10:24:01.791Z","timestamp":"2020-10-23T10:24:02.791Z","event_id":"3367f5196c494acaae85bbbd535379ac","contexts":{"trace":{"trace_id":"b156a475de54423d9c1571df97ec7eb6","span_id":"0a53026963414893","op":"http","status":"ok"},"custom":{"some-key":"some-value"}},"spans":[{"start_timestamp":"2021-03-05T08:51:12.838Z","timestamp":"2021-03-05T08:51:12.949Z","trace_id":"2b099185293344a5bfdd7ad89ebf9416","span_id":"5b95c29a5ded4281","parent_span_id":"a3b2d1d58b344b07","op":"PersonService.create","description":"desc","status":"aborted","tags":{"name":"value"}}]} diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index 574bb019214..a6f35b31a66 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -59,7 +59,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -121,6 +120,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } } diff --git a/sentry/src/test/resources/json/device.json b/sentry/src/test/resources/json/device.json index be4aabf38c2..a82f864ee28 100644 --- a/sentry/src/test/resources/json/device.json +++ b/sentry/src/test/resources/json/device.json @@ -34,7 +34,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, diff --git a/sentry/src/test/resources/json/rrweb_options_event.json b/sentry/src/test/resources/json/rrweb_options_event.json new file mode 100644 index 00000000000..1137997175f --- /dev/null +++ b/sentry/src/test/resources/json/rrweb_options_event.json @@ -0,0 +1,18 @@ +{ + "type": 5, + "timestamp": 12345678901, + "data": { + "tag": "options", + "payload": { + "unmaskedViewClasses": ["com.example.MyClass"], + "nativeSdkVersion": "7.19.1", + "errorSampleRate": 0.1, + "maskAllImages": false, + "maskAllText": false, + "maskedViewClasses": [], + "nativeSdkName": "sentry.java", + "sessionSampleRate": 0.5, + "quality": "low" + } + } +} diff --git a/sentry/src/test/resources/json/sentry_base_event.json b/sentry/src/test/resources/json/sentry_base_event.json index c889fe6bdc2..d2d1fd00881 100644 --- a/sentry/src/test/resources/json/sentry_base_event.json +++ b/sentry/src/test/resources/json/sentry_base_event.json @@ -62,7 +62,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -124,6 +123,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json index 02f3c1502ad..4ce74eaf09e 100644 --- a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json +++ b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json @@ -62,7 +62,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -124,6 +123,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_envelope_header.json b/sentry/src/test/resources/json/sentry_envelope_header.json index 5f6b3b25e78..626e9cbbc23 100644 --- a/sentry/src/test/resources/json/sentry_envelope_header.json +++ b/sentry/src/test/resources/json/sentry_envelope_header.json @@ -24,7 +24,6 @@ "release": "9ee2c92c-401e-4296-b6f0-fb3b13edd9ee", "environment": "0666ab02-6364-4135-aa59-02e8128ce052", "user_id": "c052c566-6619-45f5-a61f-172802afa39a", - "user_segment": "f7d8662b-5551-4ef8-b6a8-090f0561a530", "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e", "sample_rate": "0.00000021", "sampled": "true", diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index 7ae1ba107fc..6d421fc9936 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -197,7 +197,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -259,6 +258,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_replay_event.json b/sentry/src/test/resources/json/sentry_replay_event.json index f026c9fee47..d3970bf5b00 100644 --- a/sentry/src/test/resources/json/sentry_replay_event.json +++ b/sentry/src/test/resources/json/sentry_replay_event.json @@ -80,7 +80,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -142,6 +141,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_span.json b/sentry/src/test/resources/json/sentry_span.json index 0632c32a8a3..63f284de20f 100644 --- a/sentry/src/test/resources/json/sentry_span.json +++ b/sentry/src/test/resources/json/sentry_span.json @@ -19,19 +19,6 @@ "value": 1, "unit": "test" } - }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 1.0, - "max": 2.0, - "sum": 3.0, - "count": 2, - "tags": { - "environment": "production" - } - } - ] } } diff --git a/sentry/src/test/resources/json/sentry_span_legacy_date_format.json b/sentry/src/test/resources/json/sentry_span_legacy_date_format.json index 435043ead3f..b9a3edcb850 100644 --- a/sentry/src/test/resources/json/sentry_span_legacy_date_format.json +++ b/sentry/src/test/resources/json/sentry_span_legacy_date_format.json @@ -21,18 +21,5 @@ "value": 1, "unit": "test" } - }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 1.0, - "max": 2.0, - "sum": 3.0, - "count": 2, - "tags": { - "environment": "production" - } - } - ] } } diff --git a/sentry/src/test/resources/json/sentry_transaction.json b/sentry/src/test/resources/json/sentry_transaction.json index 7363dd35d65..33080c9686e 100644 --- a/sentry/src/test/resources/json/sentry_transaction.json +++ b/sentry/src/test/resources/json/sentry_transaction.json @@ -29,20 +29,7 @@ "value": 1, "unit": "test" } - }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 1.0, - "max": 2.0, - "sum": 3.0, - "count": 2, - "tags": { - "environment": "production" - } - } - ] - } + } } ], "type": "transaction", @@ -66,19 +53,6 @@ "unit": "test" } }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 5.0, - "max": 6.0, - "sum": 11.0, - "count": 3, - "tags": { - "environment": "production" - } - } - ] - }, "transaction_info": { "source": "custom" }, @@ -145,7 +119,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -207,6 +180,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json index a0f8a675aee..0d6ed5eb095 100644 --- a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json +++ b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json @@ -29,20 +29,7 @@ "value": 1, "unit": "test" } - }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 1.0, - "max": 2.0, - "sum": 3.0, - "count": 2, - "tags": { - "environment": "production" - } - } - ] - } + } } ], "type": "transaction", @@ -66,19 +53,6 @@ "unit": "test" } }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 5.0, - "max": 6.0, - "sum": 11.0, - "count": 3, - "tags": { - "environment": "production" - } - } - ] - }, "transaction_info": { "source": "custom" }, @@ -145,7 +119,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127, "processor_count": 4, @@ -207,6 +180,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json index 8ffb9a8f5d6..2d965be1b9c 100644 --- a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json +++ b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json @@ -28,20 +28,7 @@ "value": 1, "unit": "test" } - }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 1.0, - "max": 2.0, - "sum": 3.0, - "count": 2, - "tags": { - "environment": "production" - } - } - ] - } + } } ], "type": "transaction", @@ -57,19 +44,6 @@ "unit": "test" } }, - "_metrics_summary": { - "d:custom/background_operation@second": [ - { - "min": 5.0, - "max": 6.0, - "sum": 11.0, - "count": 3, - "tags": { - "environment": "production" - } - } - ] - }, "transaction_info": { "source": "custom" }, @@ -133,7 +107,6 @@ "boot_time": "2004-11-04T08:38:00.000Z", "timezone": "Europe/Vienna", "id": "e0fa5c8d-83f5-4e70-bc60-1e82ad30e196", - "language": "6dd45f60-111d-42d8-9204-0452cc836ad8", "connection_type": "9ceb3a6c-5292-4ed9-8665-5732495e8ed4", "battery_temperature": 0.14775127 }, @@ -177,6 +150,10 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } }, diff --git a/sentry/src/test/resources/json/span_context.json b/sentry/src/test/resources/json/span_context.json index 4a6e08bcc2d..c55841a391b 100644 --- a/sentry/src/test/resources/json/span_context.json +++ b/sentry/src/test/resources/json/span_context.json @@ -11,5 +11,9 @@ "2a5fa3f5-7b87-487f-aaa5-84567aa73642": "4781d51a-c5af-47f2-a4ed-f030c9b3e194", "29106d7d-7fa4-444f-9d34-b9d7510c69ab": "218c23ea-694a-497e-bf6d-e5f26f1ad7bd", "ba9ce913-269f-4c03-882d-8ca5e6991b14": "35a74e90-8db8-4610-a411-872cbc1030ac" + }, + "data": + { + "spanContextDataKey": "spanContextDataValue" } } diff --git a/sentry/src/test/resources/json/trace_state.json b/sentry/src/test/resources/json/trace_state.json index 6ca0e48e616..db745e52136 100644 --- a/sentry/src/test/resources/json/trace_state.json +++ b/sentry/src/test/resources/json/trace_state.json @@ -4,7 +4,6 @@ "release": "9ee2c92c-401e-4296-b6f0-fb3b13edd9ee", "environment": "0666ab02-6364-4135-aa59-02e8128ce052", "user_id": "c052c566-6619-45f5-a61f-172802afa39a", - "user_segment": "f7d8662b-5551-4ef8-b6a8-090f0561a530", "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e", "sample_rate": "0.00000021", "sampled": "true", diff --git a/sentry/src/test/resources/json/trace_state_legacy.json b/sentry/src/test/resources/json/trace_state_legacy.json deleted file mode 100644 index 14f4f904a6d..00000000000 --- a/sentry/src/test/resources/json/trace_state_legacy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "trace_id": "65bcd18546c942069ed957b15b4ace7c", - "public_key": "5d593cac-f833-4845-bb23-4eabdf720da2", - "release": "9ee2c92c-401e-4296-b6f0-fb3b13edd9ee", - "environment": "0666ab02-6364-4135-aa59-02e8128ce052", - "user": - { - "id": "c052c566-6619-45f5-a61f-172802afa39a", - "segment": "f7d8662b-5551-4ef8-b6a8-090f0561a530" - }, - "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e" -} diff --git a/sentry/src/test/resources/json/trace_state_no_sample_rate.json b/sentry/src/test/resources/json/trace_state_no_sample_rate.json index 538dc616718..ba81c185201 100644 --- a/sentry/src/test/resources/json/trace_state_no_sample_rate.json +++ b/sentry/src/test/resources/json/trace_state_no_sample_rate.json @@ -4,6 +4,5 @@ "release": "9ee2c92c-401e-4296-b6f0-fb3b13edd9ee", "environment": "0666ab02-6364-4135-aa59-02e8128ce052", "user_id": "c052c566-6619-45f5-a61f-172802afa39a", - "user_segment": "f7d8662b-5551-4ef8-b6a8-090f0561a530", "transaction": "0252ec25-cd0a-4230-bd2f-936a4585637e" } diff --git a/sentry/src/test/resources/json/user_legacy.json b/sentry/src/test/resources/json/user_legacy.json deleted file mode 100644 index f28a659494a..00000000000 --- a/sentry/src/test/resources/json/user_legacy.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "email": "c4d61c1b-c144-431e-868f-37a46be5e5f2", - "id": "efb2084b-1871-4b59-8897-b4bd9f196a01", - "username": "60c05dff-7140-4d94-9a61-c9cdd9ca9b96", - "ip_address": "51d22b77-f663-4dbe-8103-8b749d1d9a48", - "other": - { - "dc2813d0-0f66-4a3f-a995-71268f61a8fa": "991659ad-7c59-4dd3-bb89-0bd5c74014bd" - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 77b3be021da..d99f0f0e0a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,7 +17,6 @@ include( "sentry-android-ndk", "sentry-android", "sentry-android-timber", - "sentry-android-okhttp", "sentry-android-fragment", "sentry-android-navigation", "sentry-android-sqlite", @@ -42,14 +41,20 @@ include( "sentry-bom", "sentry-openfeign", "sentry-graphql", + "sentry-graphql-22", + "sentry-graphql-core", "sentry-jdbc", + "sentry-opentelemetry:sentry-opentelemetry-bootstrap", "sentry-opentelemetry:sentry-opentelemetry-core", "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", "sentry-opentelemetry:sentry-opentelemetry-agent", + "sentry-opentelemetry:sentry-opentelemetry-agentless", + "sentry-opentelemetry:sentry-opentelemetry-agentless-spring", "sentry-quartz", "sentry-okhttp", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", + "sentry-samples:sentry-samples-console-opentelemetry-noagent", "sentry-samples:sentry-samples-jul", "sentry-samples:sentry-samples-log4j2", "sentry-samples:sentry-samples-logback", @@ -57,7 +62,11 @@ include( "sentry-samples:sentry-samples-spring", "sentry-samples:sentry-samples-spring-jakarta", "sentry-samples:sentry-samples-spring-boot", + "sentry-samples:sentry-samples-spring-boot-opentelemetry", + "sentry-samples:sentry-samples-spring-boot-opentelemetry-noagent", "sentry-samples:sentry-samples-spring-boot-jakarta", + "sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry", + "sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "sentry-samples:sentry-samples-spring-boot-webflux", "sentry-samples:sentry-samples-spring-boot-webflux-jakarta", "sentry-samples:sentry-samples-netflix-dgs", @@ -68,12 +77,3 @@ include( "sentry-android-integration-tests:test-app-sentry", "sentry-samples:sentry-samples-openfeign" ) - -gradle.beforeProject { - if (project.name == "sentry-android-ndk" || project.name == "sentry-samples-android") { - exec { - logger.log(LogLevel.LIFECYCLE, "Initializing git submodules") - commandLine("git", "submodule", "update", "--init", "--recursive") - } - } -} diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh new file mode 100755 index 00000000000..e65a500b4d6 --- /dev/null +++ b/test/system-test-run-all.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +./test/system-test-run.sh "sentry-samples-spring-boot" "0" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry-noagent" "0" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "false" +./test/system-test-run.sh "sentry-samples-spring-boot-webflux-jakarta" "0" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-webflux" "0" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" diff --git a/test/system-test-run.sh b/test/system-test-run.sh new file mode 100755 index 00000000000..7f1b47bed4f --- /dev/null +++ b/test/system-test-run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +readonly SAMPLE_MODULE=$1 +readonly JAVA_AGENT=$2 +readonly JAVA_AGENT_AUTO_INIT=$3 + +test/system-test-sentry-server-start.sh +MOCK_SERVER_PID=$(cat sentry-mock-server.pid) +echo "started mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" + +test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" +SUT_PID=$(cat spring-server.pid) +echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" + +test/wait-for-spring.sh + +./gradlew :sentry-samples:${SAMPLE_MODULE}:systemTest +TESTRUN_RETVAL=$? + +echo "killing mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" +kill $SUT_PID +echo "killing spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" +kill $MOCK_SERVER_PID + +exit $TESTRUN_RETVAL diff --git a/test/system-test-sentry-server-start.sh b/test/system-test-sentry-server-start.sh index e34bdad92a3..181c77b8d79 100755 --- a/test/system-test-sentry-server-start.sh +++ b/test/system-test-sentry-server-start.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash -python3 test/system-test-sentry-server.py +python3 test/system-test-sentry-server.py > sentry-mock-server.txt 2>&1 & +echo $! > sentry-mock-server.pid diff --git a/test/system-test-sentry-server.py b/test/system-test-sentry-server.py index c98c06fe3b0..28232410622 100755 --- a/test/system-test-sentry-server.py +++ b/test/system-test-sentry-server.py @@ -15,6 +15,27 @@ version='1.1.0' appIdentifier='com.sentry.fastlane.app' +class EnvelopeStorage: + __envelopes_received = [] + + @classmethod + def add(cls, envelope): + cls.__envelopes_received.append(envelope) + + @classmethod + def get_envelopes_received(cls): + return cls.__envelopes_received + + @classmethod + def get_json(cls): + jsonObject = { + 'envelopes': cls.__envelopes_received + } + return json.dumps(jsonObject) + + @classmethod + def reset(cls): + cls.__envelopes_received.clear() class EnvelopeCount: __envelopes_received = 0 @@ -34,6 +55,10 @@ def get_json(cls): } return json.dumps(jsonObject) + @classmethod + def reset(cls): + cls.__envelopes_received = 0 + class Handler(BaseHTTPRequestHandler): body = None @@ -51,6 +76,18 @@ def do_GET(self): self.writeJSON(EnvelopeCount.get_json()) return + if self.path == "/envelopes-received": + print("Envelopes queried ") + self.writeJSON(EnvelopeStorage.get_json()) + return + + if self.path == "/reset": + print("Envelopes reset") + EnvelopeStorage.reset() + EnvelopeCount.reset() + self.writeJSON(json.dumps({})) + return + self.flushLogs() def do_POST(self): @@ -76,8 +113,9 @@ def log_request(self, code=None, size=None): if isinstance(code, HTTPStatus): code = code.value body = self.body = self.requestBody() - + if body: + EnvelopeStorage.add(str(body)) body = self.body[0:min(1000, len(body))] self.log_message('"%s" %s %s%s', self.requestline, str(code), "({} bytes)".format(len(body)) if size else '', body) @@ -117,10 +155,10 @@ def flushLogs(self): def getContent(self): length = int(self.headers['Content-Length']) content = self.rfile.read(length) - + if 'Content-Encoding' in self.headers and self.headers['Content-Encoding'] == 'gzip': content = gzip.decompress(content) - + return content diff --git a/test/system-test-spring-server-start.sh b/test/system-test-spring-server-start.sh index 533c1a9f7f6..1a6f3d5a069 100755 --- a/test/system-test-spring-server-start.sh +++ b/test/system-test-spring-server-start.sh @@ -1,4 +1,19 @@ #!/usr/bin/env bash readonly SAMPLE_MODULE=$1 -SENTRY_DSN="http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0" java -jar sentry-samples/${SAMPLE_MODULE}/build/libs/${SAMPLE_MODULE}-0.0.1-SNAPSHOT.jar +readonly JAVA_AGENT=$2 +readonly JAVA_AGENT_AUTO_INIT=$3 + +JAVA_AGENT_STRING="" + +echo "$JAVA_AGENT" + +if [[ "$JAVA_AGENT" == "1" ]]; then + JAVA_AGENT_STRING="-javaagent:$(find ./sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/ -not -name '*javadoc*' -name '*-agent-*' -not -name '*sources*' -not -name '*dontuse*' -type f)" + echo "Using Java Agent: ${JAVA_AGENT_STRING}" +fi + +echo "$JAVA_AGENT_STRING" + +SENTRY_DSN="http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0" SENTRY_AUTO_INIT=${JAVA_AGENT_AUTO_INIT} SENTRY_TRACES_SAMPLE_RATE=1.0 OTEL_TRACES_EXPORTER=none OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none java ${JAVA_AGENT_STRING} -jar sentry-samples/${SAMPLE_MODULE}/build/libs/${SAMPLE_MODULE}-0.0.1-SNAPSHOT.jar > spring-server.txt 2>&1 & +echo $! > spring-server.pid