diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dfca015d13..a19e12c1c1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @adinauer @romtsn @stefanosiano @markushi +* @adinauer @romtsn @stefanosiano @markushi @lcian diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index a0a7e2405c..f1e375ba65 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe3684e89d..9160d5c8ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true @@ -35,7 +35,7 @@ jobs: run: make preMerge - name: Upload coverage to Codecov - uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # pin@v4 + uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # pin@v4 with: name: sentry-java fail_ci_if_error: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 30ad0b95d7..22feb2486f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,12 +34,12 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true - name: Initialize CodeQL - uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # pin@v2 + uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # pin@v2 with: languages: 'java' @@ -48,4 +48,4 @@ jobs: ./gradlew buildForCodeQL - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # pin@v2 + uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # pin@v2 diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index ee69bb0b5c..0fe847cd55 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index 564cf4c43d..dc0e2f5da2 100644 --- a/.github/workflows/generate-javadocs.yml +++ b/.github/workflows/generate-javadocs.yml @@ -20,7 +20,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index 1b6295bc5b..c04d2bc624 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -37,7 +37,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true @@ -86,7 +86,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 201b1551f9..112dc8ad25 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index 2b1bbeacc2..0bf4b12dd6 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 0883d940f5..4de0e49e1a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -26,7 +26,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd08109449..c15509c56d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e8bb0d77e8..b4d4233b08 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -56,7 +56,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@3839b20885c2c3507be5f0521853826f4b37038a # pin@v3 + uses: gradle/actions/setup-gradle@8790d96bb8fdd8ae7edfb2eada090c650b257f27 # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 30aba6f133..4608e9bdf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,28 @@ # Changelog +## Unreleased + +### Features + +- Add `options.ignoredErrors` to filter out errors that match a certain String or Regex ([#4083](https://github.com/getsentry/sentry-java/pull/4083)) + - The matching is attempted on `event.message`, `event.formatted`, and `{event.throwable.class.name}: {event.throwable.message}` + - Can be set in `sentry.properties`, e.g. `ignored-errors=Some error,Another .*` + - Can be set in environment variables, e.g. `SENTRY_IGNORED_ERRORS=Some error,Another .*` + - For Spring Boot, it can be set in `application.properties`, e.g. `sentry.ignored-errors=Some error,Another .*` + +### Fixes + +- Avoid logging an error when a float is passed in the manifest ([#4031](https://github.com/getsentry/sentry-java/pull/4031)) +- Add `request` details to transactions created through OpenTelemetry ([#4098](https://github.com/getsentry/sentry-java/pull/4098)) + - We now add HTTP request method and URL where Sentry expects it to display it in Sentry UI +- Remove `java.lang.ClassNotFoundException` debug logs when searching for OpenTelemetry marker classes ([#4091](https://github.com/getsentry/sentry-java/pull/4091)) + - There was up to three of these, one for `io.sentry.opentelemetry.agent.AgentMarker`, `io.sentry.opentelemetry.agent.AgentlessMarker` and `io.sentry.opentelemetry.agent.AgentlessSpringMarker`. + - These were not indicators of something being wrong but rather the SDK looking at what is available at runtime to configure itself accordingly. + ## 8.0.0 +### Summary + Version 8 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: - `Hub` has been replaced by `Scopes` @@ -13,6 +34,8 @@ Version 8 of the Sentry Android/Java SDK brings a variety of features and fixes. - We now support GraphQL v22 (`sentry-graphql-22`) - Metrics have been removed +Please take a look at [our migration guide in docs](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0). + ### Sentry Self-hosted Compatibility This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or higher. If you are using an older version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise), you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action is required. @@ -60,12 +83,12 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or - 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. It can be manipulated using `Sentry.configureScope(ScopeType.CURRENT, (scope) -> { ... })`. -- `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". +- `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 [our migration guide](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0). - 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. +- `Hub` is deprecated in favor of `Scopes`, alongside some `Hub` relevant APIs. More details can be found in [our migration guide](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0). - Send file name and path only if `isSendDefaultPii` is `true` ([#3919](https://github.com/getsentry/sentry-java/pull/3919)) - (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. @@ -146,7 +169,7 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or - 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)) - 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 + - Also does not emit `transaction.data` in `extras` anymore - 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)) diff --git a/LICENSE b/LICENSE index f49694a15b..6b8b8d58af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2024 Sentry +Copyright (c) 2019 Sentry Copyright (c) 2015 Salomon BRYS for Android ANRWatchDog Permission is hereby granted, free of charge, to any person obtaining a copy 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 fcb755ec02..1bad409125 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 @@ -475,7 +475,10 @@ private static boolean readBool( private static @NotNull Double readDouble( final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { // manifest meta-data only reads float - final Double value = ((Number) metadata.getFloat(key, metadata.getInt(key, -1))).doubleValue(); + double value = ((Float) metadata.getFloat(key, -1)).doubleValue(); + if (value == -1) { + value = ((Integer) metadata.getInt(key, -1)).doubleValue(); + } logger.log(SentryLevel.DEBUG, key + " read: " + value); return value; } 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 fd2099a730..1e9bb60416 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -1,3 +1,8 @@ +public final class io/sentry/opentelemetry/OpenTelemetryAttributesExtractor { + public fun ()V + public fun extract (Lio/opentelemetry/sdk/trace/data/SpanData;Lio/sentry/ISpan;Lio/sentry/IScope;)V +} + public final class io/sentry/opentelemetry/OpenTelemetryLinkErrorEventProcessor : io/sentry/EventProcessor { public fun ()V public fun getOrder ()Ljava/lang/Long; diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java new file mode 100644 index 0000000000..431b4d274e --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java @@ -0,0 +1,90 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.HttpAttributes; +import io.opentelemetry.semconv.ServerAttributes; +import io.opentelemetry.semconv.UrlAttributes; +import io.sentry.IScope; +import io.sentry.ISpan; +import io.sentry.protocol.Request; +import io.sentry.util.UrlUtils; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class OpenTelemetryAttributesExtractor { + + public void extract( + final @NotNull SpanData otelSpan, + final @NotNull ISpan sentrySpan, + final @NotNull IScope scope) { + final @NotNull Attributes attributes = otelSpan.getAttributes(); + addRequestAttributesToScope(attributes, scope); + } + + private void addRequestAttributesToScope(Attributes attributes, IScope scope) { + if (scope.getRequest() == null) { + scope.setRequest(new Request()); + } + final @Nullable Request request = scope.getRequest(); + if (request != null) { + final @Nullable String requestMethod = attributes.get(HttpAttributes.HTTP_REQUEST_METHOD); + if (requestMethod != null) { + request.setMethod(requestMethod); + } + + if (request.getUrl() == null) { + final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL); + if (urlFull != null) { + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(urlFull); + urlDetails.applyToRequest(request); + } + } + + if (request.getUrl() == null) { + final String urlString = buildUrlString(attributes); + if (!urlString.isEmpty()) { + request.setUrl(urlString); + } + } + + if (request.getQueryString() == null) { + final @Nullable String query = attributes.get(UrlAttributes.URL_QUERY); + if (query != null) { + request.setQueryString(query); + } + } + } + } + + private @NotNull String buildUrlString(final @NotNull Attributes attributes) { + final @Nullable String scheme = attributes.get(UrlAttributes.URL_SCHEME); + final @Nullable String serverAddress = attributes.get(ServerAttributes.SERVER_ADDRESS); + final @Nullable Long serverPort = attributes.get(ServerAttributes.SERVER_PORT); + final @Nullable String path = attributes.get(UrlAttributes.URL_PATH); + + if (scheme == null || serverAddress == null) { + return ""; + } + + final @NotNull StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(scheme); + urlBuilder.append("://"); + + if (serverAddress != null) { + urlBuilder.append(serverAddress); + if (serverPort != null) { + urlBuilder.append(":"); + urlBuilder.append(serverPort); + } + } + + if (path != null) { + urlBuilder.append(path); + } + + return urlBuilder.toString(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java index 7851f07550..4d2e7545c6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -19,6 +19,7 @@ import io.sentry.ISpan; import io.sentry.ITransaction; import io.sentry.Instrumenter; +import io.sentry.ScopeType; import io.sentry.ScopesAdapter; import io.sentry.SentryDate; import io.sentry.SentryInstantDate; @@ -50,6 +51,8 @@ public final class SentrySpanExporter implements SpanExporter { private final @NotNull SentryWeakSpanStorage spanStorage = SentryWeakSpanStorage.getInstance(); private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor = new SpanDescriptionExtractor(); + private final @NotNull OpenTelemetryAttributesExtractor attributesExtractor = + new OpenTelemetryAttributesExtractor(); private final @NotNull IScopes scopes; private final @NotNull List attributeKeysToRemove = @@ -267,8 +270,10 @@ private void transferSpanDetails( spanStorage.getSentrySpan(span.getSpanContext()); final @Nullable IScopes scopesMaybe = sentrySpanMaybe != null ? sentrySpanMaybe.getScopes() : null; - final @NotNull IScopes scopesToUse = + final @NotNull IScopes scopesToUseBeforeForking = scopesMaybe == null ? ScopesAdapter.getInstance() : scopesMaybe; + final @NotNull IScopes scopesToUse = + scopesToUseBeforeForking.forkedCurrentScope("SentrySpanExporter.createTransaction"); final @NotNull OtelSpanInfo spanInfo = spanDescriptionExtractor.extractSpanInfo(span, sentrySpanMaybe); @@ -331,6 +336,9 @@ private void transferSpanDetails( setOtelSpanKind(span, sentryTransaction); transferSpanDetails(sentrySpanMaybe, sentryTransaction); + scopesToUse.configureScope( + ScopeType.CURRENT, scope -> attributesExtractor.extract(span, sentryTransaction, scope)); + return sentryTransaction; } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt new file mode 100644 index 0000000000..f962cfa594 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt @@ -0,0 +1,201 @@ +package io.sentry.opentelemetry + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.sdk.internal.AttributesMap +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.UrlAttributes +import io.sentry.ISpan +import io.sentry.Scope +import io.sentry.SentryOptions +import io.sentry.protocol.Request +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class OpenTelemetryAttributesExtractorTest { + + private class Fixture { + val spanData = mock() + val attributes = AttributesMap.create(100, 100) + val sentrySpan = mock() + val options = SentryOptions.empty() + val scope = Scope(options) + + init { + whenever(spanData.attributes).thenReturn(attributes) + } + } + + private val fixture = Fixture() + + @Test + fun `sets URL based on OTel attributes`() { + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + UrlAttributes.URL_QUERY to "q=123456&b=X", + ServerAttributes.SERVER_ADDRESS to "io.sentry", + ServerAttributes.SERVER_PORT to 8081L + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("https://io.sentry:8081/path/to/123") + thenQueryIsSetTo("q=123456&b=X") + } + + @Test + fun `when there is an existing request on scope it is filled with more details`() { + fixture.scope.request = Request().also { it.bodySize = 123L } + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + UrlAttributes.URL_QUERY to "q=123456&b=X", + ServerAttributes.SERVER_ADDRESS to "io.sentry", + ServerAttributes.SERVER_PORT to 8081L + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("https://io.sentry:8081/path/to/123") + thenQueryIsSetTo("q=123456&b=X") + assertEquals(123L, fixture.scope.request!!.bodySize) + } + + @Test + fun `when there is an existing request with url on scope it is kept`() { + fixture.scope.request = Request().also { + it.url = "http://docs.sentry.io:3000/platform" + it.queryString = "s=abc" + } + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + UrlAttributes.URL_QUERY to "q=123456&b=X", + ServerAttributes.SERVER_ADDRESS to "io.sentry", + ServerAttributes.SERVER_PORT to 8081L + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("http://docs.sentry.io:3000/platform") + thenQueryIsSetTo("s=abc") + } + + @Test + fun `when there is an existing request with url on scope it is kept with URL_FULL`() { + fixture.scope.request = Request().also { + it.url = "http://docs.sentry.io:3000/platform" + it.queryString = "s=abc" + } + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081/path/to/123?q=123456&b=X" + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("http://docs.sentry.io:3000/platform") + thenQueryIsSetTo("s=abc") + } + + @Test + fun `sets URL based on OTel attributes without port`() { + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + ServerAttributes.SERVER_ADDRESS to "io.sentry" + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("https://io.sentry/path/to/123") + } + + @Test + fun `sets URL based on OTel attributes without path`() { + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https", + ServerAttributes.SERVER_ADDRESS to "io.sentry" + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsSetTo("https://io.sentry") + } + + @Test + fun `does not set URL if server address is missing`() { + givenAttributes( + mapOf( + UrlAttributes.URL_SCHEME to "https" + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsNotSet() + } + + @Test + fun `does not set URL if scheme is missing`() { + givenAttributes( + mapOf( + ServerAttributes.SERVER_ADDRESS to "io.sentry" + ) + ) + + whenExtractingAttributes() + + thenRequestIsSet() + thenUrlIsNotSet() + } + + private fun givenAttributes(map: Map, Any>) { + map.forEach { k, v -> + fixture.attributes.put(k, v) + } + } + + private fun whenExtractingAttributes() { + OpenTelemetryAttributesExtractor().extract(fixture.spanData, fixture.sentrySpan, fixture.scope) + } + + private fun thenRequestIsSet() { + assertNotNull(fixture.scope.request) + } + + private fun thenUrlIsSetTo(expected: String) { + assertEquals(expected, fixture.scope.request!!.url) + } + + private fun thenUrlIsNotSet() { + assertNull(fixture.scope.request!!.url) + } + + private fun thenQueryIsSetTo(expected: String) { + assertEquals(expected, fixture.scope.request!!.queryString) + } +} diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index e559502e62..3498c90319 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -174,6 +174,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", + "sentry.ignored-errors=Some error,Another .*", "sentry.ignored-transactions=transactionName1,transactionNameB", "sentry.enable-backpressure-handling=false", "sentry.enable-spotlight=true", @@ -215,6 +216,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly(FilterString("slug1"), FilterString("slugB")) + assertThat(options.ignoredErrors).containsOnly(FilterString("Some error"), FilterString("Another .*")) assertThat(options.ignoredTransactions).containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.isForceInit).isEqualTo(true) diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 2512fe1f11..cd105dab44 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -173,6 +173,7 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", + "sentry.ignored-errors=Some error,Another .*", "sentry.ignored-transactions=transactionName1,transactionNameB", "sentry.enable-backpressure-handling=false", "sentry.enable-spotlight=true", @@ -214,6 +215,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly(FilterString("slug1"), FilterString("slugB")) + assertThat(options.ignoredErrors).containsOnly(FilterString("Some error"), FilterString("Another .*")) assertThat(options.ignoredTransactions).containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.isForceInit).isEqualTo(true) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a0d559f12f..642b67ec06 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -452,6 +452,7 @@ public final class io/sentry/ExternalOptions { public fun getEnvironment ()Ljava/lang/String; public fun getIdleTimeout ()Ljava/lang/Long; public fun getIgnoredCheckIns ()Ljava/util/List; + public fun getIgnoredErrors ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getIgnoredTransactions ()Ljava/util/List; public fun getInAppExcludes ()Ljava/util/List; @@ -491,6 +492,7 @@ public final class io/sentry/ExternalOptions { public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V + public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V @@ -2814,6 +2816,7 @@ public class io/sentry/SentryOptions { public fun addContextTag (Ljava/lang/String;)V public fun addEventProcessor (Lio/sentry/EventProcessor;)V public fun addIgnoredCheckIn (Ljava/lang/String;)V + public fun addIgnoredError (Ljava/lang/String;)V public fun addIgnoredExceptionForType (Ljava/lang/Class;)V public fun addIgnoredSpanOrigin (Ljava/lang/String;)V public fun addIgnoredTransaction (Ljava/lang/String;)V @@ -2855,6 +2858,7 @@ public class io/sentry/SentryOptions { public fun getGestureTargetLocators ()Ljava/util/List; public fun getIdleTimeout ()Ljava/lang/Long; public fun getIgnoredCheckIns ()Ljava/util/List; + public fun getIgnoredErrors ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getIgnoredSpanOrigins ()Ljava/util/List; public fun getIgnoredTransactions ()Ljava/util/List; @@ -2987,6 +2991,7 @@ public class io/sentry/SentryOptions { public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V + public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredSpanOrigins (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setInitPriority (Lio/sentry/InitPriority;)V @@ -6047,6 +6052,11 @@ public final class io/sentry/util/DebugMetaPropertiesApplier { public static fun getProguardUuid (Ljava/util/Properties;)Ljava/lang/String; } +public final class io/sentry/util/ErrorUtils { + public fun ()V + public static fun isIgnored (Ljava/util/List;Lio/sentry/SentryEvent;)Z +} + public final class io/sentry/util/EventProcessorUtils { public fun ()V public static fun unwrap (Ljava/util/List;)Ljava/util/List; @@ -6055,6 +6065,7 @@ public final class io/sentry/util/EventProcessorUtils { public final class io/sentry/util/ExceptionUtils { public fun ()V public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; + public static fun isIgnored (Ljava/util/Set;Ljava/lang/Throwable;)Z } public final class io/sentry/util/FileUtils { diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index d9b075e1c8..ed2b4e1103 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -1,10 +1,7 @@ package io.sentry; import io.sentry.config.PropertiesProvider; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; @@ -39,6 +36,7 @@ public final class ExternalOptions { private @Nullable Long idleTimeout; private final @NotNull Set> ignoredExceptionsForType = new CopyOnWriteArraySet<>(); + private @Nullable List ignoredErrors; private @Nullable Boolean printUncaughtStackTrace; private @Nullable Boolean sendClientReports; private @NotNull Set bundleIds = new CopyOnWriteArraySet<>(); @@ -130,6 +128,8 @@ public final class ExternalOptions { } options.setIdleTimeout(propertiesProvider.getLongProperty("idle-timeout")); + options.setIgnoredErrors(propertiesProvider.getList("ignored-errors")); + options.setEnabled(propertiesProvider.getBooleanProperty("enabled")); options.setEnablePrettySerializationOutput( @@ -373,6 +373,14 @@ public void setIdleTimeout(final @Nullable Long idleTimeout) { this.idleTimeout = idleTimeout; } + public @Nullable List getIgnoredErrors() { + return ignoredErrors; + } + + public void setIgnoredErrors(final @Nullable List ignoredErrors) { + this.ignoredErrors = ignoredErrors; + } + public @Nullable Boolean getSendClientReports() { return sendClientReports; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 1cfbf60313..277be87aee 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -11,12 +11,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; import io.sentry.transport.RateLimiter; -import io.sentry.util.CheckInUtils; -import io.sentry.util.HintUtils; -import io.sentry.util.Objects; -import io.sentry.util.Random; -import io.sentry.util.SentryRandom; -import io.sentry.util.TracingUtils; +import io.sentry.util.*; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; @@ -103,7 +98,8 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul if (event != null) { final Throwable eventThrowable = event.getThrowable(); - if (eventThrowable != null && options.containsIgnoredExceptionForType(eventThrowable)) { + if (eventThrowable != null + && ExceptionUtils.isIgnored(options.getIgnoredExceptionsForType(), eventThrowable)) { options .getLogger() .log( @@ -115,6 +111,19 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); return SentryId.EMPTY_ID; } + + if (ErrorUtils.isIgnored(options.getIgnoredErrors(), event)) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Event was dropped as it matched a string/pattern in ignoredErrors", + event.getMessage()); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); + return SentryId.EMPTY_ID; + } } if (shouldApplyScopeData(event, hint)) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3a6d75789a..2617cd3dfa 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -69,6 +69,12 @@ public class SentryOptions { private final @NotNull Set> ignoredExceptionsForType = new CopyOnWriteArraySet<>(); + /** + * Strings or regex patterns that possible error messages for an event will be tested against. If + * there is a match, the captured event will not be sent to Sentry. + */ + private @Nullable List ignoredErrors = null; + /** * Code that provides middlewares, bindings or hooks into certain frameworks or environments, * along with code that inserts those bindings and activates them. @@ -1572,6 +1578,55 @@ boolean containsIgnoredExceptionForType(final @NotNull Throwable throwable) { return this.ignoredExceptionsForType.contains(throwable.getClass()); } + /** + * Returns the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @return the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine + * if an event shall be sent to Sentry or ignored + */ + public @Nullable List getIgnoredErrors() { + return ignoredErrors; + } + + /** + * Sets the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @param ignoredErrors the list of strings/regex patterns + */ + public void setIgnoredErrors(final @Nullable List ignoredErrors) { + if (ignoredErrors == null) { + this.ignoredErrors = null; + } else { + @NotNull final List patterns = new ArrayList<>(); + for (String pattern : ignoredErrors) { + if (pattern != null && !pattern.isEmpty()) { + patterns.add(new FilterString(pattern)); + } + } + + this.ignoredErrors = patterns; + } + } + + /** + * Adds an item to the list of strings/regex patterns that `event.message`, `event.formatted`, and + * `{event.throwable.class.name}: {event.throwable.message}` are checked against to determine if + * an event shall be sent to Sentry or ignored. + * + * @param pattern the string/regex pattern + */ + public void addIgnoredError(final @NotNull String pattern) { + if (ignoredErrors == null) { + ignoredErrors = new ArrayList<>(); + } + ignoredErrors.add(new FilterString(pattern)); + } + /** * Returns the maximum number of spans that can be attached to single transaction. * @@ -2801,6 +2856,10 @@ public void merge(final @NotNull ExternalOptions options) { final List ignoredTransactions = new ArrayList<>(options.getIgnoredTransactions()); setIgnoredTransactions(ignoredTransactions); } + if (options.getIgnoredErrors() != null) { + final List ignoredExceptions = new ArrayList<>(options.getIgnoredErrors()); + setIgnoredErrors(ignoredExceptions); + } if (options.isEnableBackpressureHandling() != null) { setEnableBackpressureHandling(options.isEnableBackpressureHandling()); } diff --git a/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java b/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java index b491ab5278..3bf7eac1a2 100644 --- a/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java +++ b/sentry/src/main/java/io/sentry/opentelemetry/OpenTelemetryUtil.java @@ -1,5 +1,6 @@ package io.sentry.opentelemetry; +import io.sentry.NoOpLogger; import io.sentry.SentryOpenTelemetryMode; import io.sentry.SentryOptions; import io.sentry.util.LoadClass; @@ -29,15 +30,15 @@ public static void applyIgnoredSpanOrigins( final @NotNull SentryOpenTelemetryMode openTelemetryMode = options.getOpenTelemetryMode(); if (SentryOpenTelemetryMode.AUTO.equals(openTelemetryMode)) { if (loadClass.isClassAvailable( - "io.sentry.opentelemetry.agent.AgentMarker", options.getLogger())) { + "io.sentry.opentelemetry.agent.AgentMarker", NoOpLogger.getInstance())) { return SpanUtils.ignoredSpanOriginsForOpenTelemetry(SentryOpenTelemetryMode.AGENT); } if (loadClass.isClassAvailable( - "io.sentry.opentelemetry.agent.AgentlessMarker", options.getLogger())) { + "io.sentry.opentelemetry.agent.AgentlessMarker", NoOpLogger.getInstance())) { return SpanUtils.ignoredSpanOriginsForOpenTelemetry(SentryOpenTelemetryMode.AGENTLESS); } if (loadClass.isClassAvailable( - "io.sentry.opentelemetry.agent.AgentlessSpringMarker", options.getLogger())) { + "io.sentry.opentelemetry.agent.AgentlessSpringMarker", NoOpLogger.getInstance())) { return SpanUtils.ignoredSpanOriginsForOpenTelemetry( SentryOpenTelemetryMode.AGENTLESS_SPRING); } diff --git a/sentry/src/main/java/io/sentry/util/ErrorUtils.java b/sentry/src/main/java/io/sentry/util/ErrorUtils.java new file mode 100644 index 0000000000..cb8b3dbce9 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/ErrorUtils.java @@ -0,0 +1,57 @@ +package io.sentry.util; + +import io.sentry.FilterString; +import io.sentry.SentryEvent; +import io.sentry.protocol.Message; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class ErrorUtils { + + /** Checks if an error has been ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @Nullable List ignoredErrors, final @NotNull SentryEvent event) { + if (event == null || ignoredErrors == null || ignoredErrors.isEmpty()) { + return false; + } + + final @NotNull Set possibleMessages = new HashSet<>(); + + final @Nullable Message eventMessage = event.getMessage(); + if (eventMessage != null) { + final @Nullable String stringMessage = eventMessage.getMessage(); + if (stringMessage != null) { + possibleMessages.add(stringMessage); + } + final @Nullable String formattedMessage = eventMessage.getFormatted(); + if (formattedMessage != null) { + possibleMessages.add(formattedMessage); + } + } + final @Nullable Throwable throwable = event.getThrowable(); + if (throwable != null) { + possibleMessages.add(throwable.toString()); + } + + for (final @NotNull FilterString filter : ignoredErrors) { + if (possibleMessages.contains(filter.getFilterString())) { + return true; + } + } + + for (final @NotNull FilterString filter : ignoredErrors) { + for (final @NotNull String message : possibleMessages) { + if (filter.matches(message)) { + return true; + } + } + } + + return false; + } +} diff --git a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java index 04285751c1..9d6033a96c 100644 --- a/sentry/src/main/java/io/sentry/util/ExceptionUtils.java +++ b/sentry/src/main/java/io/sentry/util/ExceptionUtils.java @@ -1,5 +1,6 @@ package io.sentry.util; +import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,4 +21,12 @@ public final class ExceptionUtils { } return rootCause; } + + /** Checks if an exception has been ignored. */ + @ApiStatus.Internal + public static boolean isIgnored( + final @NotNull Set> ignoredExceptionsForType, + final @NotNull Throwable throwable) { + return ignoredExceptionsForType.contains(throwable.getClass()); + } } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index b25f67405c..f32b6cf8c0 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -209,6 +209,15 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with ignored error patterns using external properties`() { + val logger = mock() + withPropertiesFile("ignored-errors=Some error,Another .*", logger) { options -> + assertTrue(options.ignoredErrors!!.contains("Some error")) + assertTrue(options.ignoredErrors!!.contains("Another .*")) + } + } + @Test fun `creates options with single bundle ID using external properties`() { withPropertiesFile("bundle-ids=12ea7a02-46ac-44c0-a5bb-6d1fd9586411") { options -> diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index da57f5376e..63052022eb 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -15,6 +15,7 @@ import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism +import io.sentry.protocol.Message import io.sentry.protocol.Request import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryException @@ -1758,6 +1759,65 @@ class SentryClientTest { verify(fixture.transport, never()).send(any(), anyOrNull()) } + @Test + fun `when event message matches string in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("hello") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "hello" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when event message matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("hello .*") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "hello world" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when event message does not match regex pattern in ignoredErrors, capturing event sends it`() { + fixture.sentryOptions.addIgnoredError("hello .*") + val sut = fixture.getSut() + val event = SentryEvent() + val message = Message() + message.message = "test" + event.setMessage(message) + sut.captureEvent(event) + verify(fixture.transport).send(any(), anyOrNull()) + } + + @Test + fun `when exception message matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError(".*hello .*") + val sut = fixture.getSut() + sut.captureException(RuntimeException("hello world")) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when class matches regex pattern in ignoredErrors, capturing event does not send it`() { + fixture.sentryOptions.addIgnoredError("java\\.lang\\..*") + val sut = fixture.getSut() + sut.captureException(RuntimeException("hello world")) + verify(fixture.transport, never()).send(any(), anyOrNull()) + } + + @Test + fun `when ignoredExceptionsForType and ignoredErrors are not explicitly specified, capturing event sends event`() { + val sut = fixture.getSut() + sut.captureException(RuntimeException("test")) + verify(fixture.transport).send(any(), anyOrNull()) + } + @Test fun `screenshot is added to the envelope from the hint`() { val sut = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 46482a1083..278c351916 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -326,6 +326,7 @@ class SentryOptionsTest { externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") externalOptions.ignoredTransactions = listOf("transactionName1", "transaction-name-B") + externalOptions.ignoredErrors = listOf("Some error", "Another .*") externalOptions.isEnableBackpressureHandling = false externalOptions.maxRequestBodySize = SentryOptions.RequestSize.MEDIUM externalOptions.isSendDefaultPii = true @@ -370,6 +371,7 @@ class SentryOptionsTest { assertFalse(options.isSendModules) assertEquals(listOf(FilterString("slug1"), FilterString("slug-B")), options.ignoredCheckIns) assertEquals(listOf(FilterString("transactionName1"), FilterString("transaction-name-B")), options.ignoredTransactions) + assertEquals(listOf(FilterString("Some error"), FilterString("Another .*")), options.ignoredErrors) assertFalse(options.isEnableBackpressureHandling) assertTrue(options.isForceInit) assertNotNull(options.cron) diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index e9a38631cc..ade4ab88b9 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -28,6 +28,8 @@ import io.sentry.hints.DiskFlushNotification import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.test.getProperty +import io.sentry.test.injectForField import io.sentry.util.HintUtils import org.awaitility.kotlin.await import org.mockito.kotlin.eq @@ -38,6 +40,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import java.io.File +import java.util.Timer import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test @@ -361,20 +364,16 @@ class RateLimiterTest { @Test fun `close cancels the timer`() { val rateLimiter = fixture.getSUT() - whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 1, 2001) + val timer = mock() + rateLimiter.injectForField("timer", timer) - val applied = AtomicBoolean(true) - rateLimiter.addRateLimitObserver { - applied.set(rateLimiter.isActiveForCategory(Replay)) - } - - rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) + // When the rate limiter is closed rateLimiter.close() - // 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()) + // Then the timer is cancelled + verify(timer).cancel() + + // And is removed by the rateLimiter + assertNull(rateLimiter.getProperty("timer")) } }