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 a13bc952829..0f322481e69 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 @@ -160,7 +160,7 @@ constructor( operationType: String?, operationId: String?, ): ISpan { - val urlDetails = UrlUtils.parse(request.url) + val urlDetails = UrlUtils.parse(request.url, scopes.options.dataCollectionResolver) val method = request.method.name val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql" @@ -232,7 +232,13 @@ constructor( span.finish() } - val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) + val breadcrumb = + Breadcrumb.http( + request.url, + request.method.name, + statusCode, + scopes.options.dataCollectionResolver, + ) request.body?.contentLength.ifHasValidLength { contentLength -> breadcrumb.setData("request_body_size", contentLength) @@ -351,7 +357,7 @@ constructor( // url will be: https://api.github.com/users/getsentry/repos/ // ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/ // but that's not possible - val urlDetails = UrlUtils.parse(request.url) + val urlDetails = UrlUtils.parse(request.url, scopes.options.dataCollectionResolver) // return if its not a target match if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) { diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index 28fb646c31c..0a16c669914 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -159,7 +159,7 @@ constructor( operationType: String?, operationId: String?, ): ISpan { - val urlDetails = UrlUtils.parse(request.url) + val urlDetails = UrlUtils.parse(request.url, scopes.options.dataCollectionResolver) val method = request.method.name val operation = if (operationType != null) "http.graphql.$operationType" else "http.graphql" @@ -231,7 +231,13 @@ constructor( span.finish() } - val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) + val breadcrumb = + Breadcrumb.http( + request.url, + request.method.name, + statusCode, + scopes.options.dataCollectionResolver, + ) request.body?.contentLength.ifHasValidLength { contentLength -> breadcrumb.setData("request_body_size", contentLength) @@ -350,7 +356,7 @@ constructor( // url will be: https://api.github.com/users/getsentry/repos/ // ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/ // but that's not possible - val urlDetails = UrlUtils.parse(request.url) + val urlDetails = UrlUtils.parse(request.url, scopes.options.dataCollectionResolver) // return if it's not a target match if (!PropagationTargetsUtils.contain(failedRequestTargets, urlDetails.urlOrFallback)) { 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 b4fc25e7be2..cb7df6472dd 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -198,7 +198,12 @@ class SentryApolloInterceptor( val httpRequest = httpResponse.request() val breadcrumb = - Breadcrumb.http(httpRequest.url().toString(), httpRequest.method(), httpResponse.code()) + Breadcrumb.http( + httpRequest.url().toString(), + httpRequest.method(), + httpResponse.code(), + scopes.options.dataCollectionResolver, + ) httpRequest.body()?.contentLength().ifHasValidLength { contentLength -> breadcrumb.setData("request_body_size", contentLength) diff --git a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt index 1a569012558..793911a8f49 100644 --- a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt +++ b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt @@ -25,7 +25,7 @@ internal object SentryKtorClientUtils { request: HttpRequest, response: HttpResponse, ) { - val urlDetails = UrlUtils.parse(request.url.toString()) + val urlDetails = UrlUtils.parse(request.url.toString(), scopes.options.dataCollectionResolver) val mechanism = Mechanism().apply { type = "SentryKtorClientPlugin" } val exception = @@ -116,7 +116,12 @@ internal object SentryKtorClientUtils { endTimestamp: SentryDate?, ) { val breadcrumb = - Breadcrumb.http(request.url.toString(), request.method.value, response.status.value) + Breadcrumb.http( + request.url.toString(), + request.method.value, + response.status.value, + scopes.options.dataCollectionResolver, + ) breadcrumb.setData( SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, response.contentLength(), 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 7475f09443b..48dd678dd67 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -34,7 +34,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques private var method: String init { - val urlDetails = UrlUtils.parse(request.url.toString()) + val urlDetails = UrlUtils.parse(request.url.toString(), scopes.options.dataCollectionResolver) url = urlDetails.urlOrFallback method = request.method @@ -62,7 +62,7 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques * due to interceptors. */ fun setRequest(request: Request) { - val urlDetails = UrlUtils.parse(request.url.toString()) + val urlDetails = UrlUtils.parse(request.url.toString(), scopes.options.dataCollectionResolver) url = urlDetails.urlOrFallback val host: String = request.url.host @@ -78,8 +78,8 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques breadcrumb.setData("url", urlDetails.url!!) } breadcrumb.setData("method", method.uppercase()) - if (urlDetails.query != null) { - breadcrumb.setData("http.query", urlDetails.query!!) + urlDetails.query?.let { + breadcrumb.setData("http.query", it) } if (urlDetails.fragment != null) { breadcrumb.setData("http.fragment", urlDetails.fragment!!) 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 7031be3b0b3..ed704966610 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -81,7 +81,7 @@ public open class SentryOkHttpInterceptor( override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - val urlDetails = UrlUtils.parse(request.url.toString()) + val urlDetails = UrlUtils.parse(request.url.toString(), scopes.options.dataCollectionResolver) val url = urlDetails.urlOrFallback val method = request.method @@ -235,7 +235,13 @@ public open class SentryOkHttpInterceptor( startTimestamp: Long, networkDetailData: NetworkRequestData?, ) { - val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) + val breadcrumb = + Breadcrumb.http( + request.url.toString(), + request.method, + code, + scopes.options.dataCollectionResolver, + ) // Track request and response body sizes for the breadcrumb request.body?.contentLength().ifHasValidLength { 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 07fcac12f06..ce8759e5715 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt @@ -21,7 +21,7 @@ internal object SentryOkHttpUtils { // url will be: https://api.github.com/users/getsentry/repos/ // ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/ // but that's not possible - val urlDetails = UrlUtils.parse(request.url.toString()) + val urlDetails = UrlUtils.parse(request.url.toString(), scopes.options.dataCollectionResolver) val mechanism = Mechanism().apply { type = "SentryOkHttpInterceptor" } val exception = 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 9f7d8bc18fb..7b49105dc13 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -504,6 +504,26 @@ class SentryOkHttpInterceptorTest { ) } + @Test + fun `data collection filters failed request query parameters`() { + val sut = + fixture.getSut( + captureFailedRequests = true, + httpStatusCode = 500, + optionsConfiguration = { it.dataCollection.setUserInfo(false) }, + ) + + sut.newCall(getRequest(url = "/hello?name=value&token=secret")).execute() + + verify(fixture.scopes) + .captureEvent( + check { + assertEquals("name=value&token=[Filtered]", it.request!!.queryString) + }, + any(), + ) + } + @Test fun `captures an error event with request body size`() { val sut = fixture.getSut(captureFailedRequests = true, httpStatusCode = 500) 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 acd73bbec7c..520828c0a75 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -73,7 +73,8 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O 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 UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(request.url(), scopes.getOptions().getDataCollectionResolver()); final @NotNull String method = request.httpMethod().name(); span.setDescription(method + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); @@ -158,7 +159,8 @@ private void addBreadcrumb(final @NotNull Request request, final @Nullable Respo Breadcrumb.http( request.url(), request.httpMethod().name(), - response != null ? response.status() : null); + response != null ? response.status() : null, + scopes.getOptions().getDataCollectionResolver()); breadcrumb.setData("request_body_size", request.body() != null ? request.body().length : 0); if (response != null && response.body() != null && response.body().length() != null) { breadcrumb.setData("response_body_size", response.body().length()); 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 index 015e56d7949..30d5b648c03 100644 --- 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 @@ -52,7 +52,8 @@ private void addRequestAttributesToScope( if (request.getUrl() == null) { final @Nullable String url = extractUrl(attributes, options); if (url != null) { - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(url, options.getDataCollectionResolver()); urlDetails.applyToRequest(request); } } @@ -60,7 +61,8 @@ private void addRequestAttributesToScope( if (request.getQueryString() == null) { final @Nullable String query = attributes.get(UrlAttributes.URL_QUERY); if (query != null) { - request.setQueryString(query); + request.setQueryString( + UrlUtils.filterQueryParams(query, options.getDataCollectionResolver())); } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt index 01efc74164f..2d310345050 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt @@ -53,6 +53,36 @@ class OpenTelemetryAttributesExtractorTest { thenQueryIsSetTo("q=123456&b=X") } + @Test + fun `data collection filters URL query attributes`() { + fixture.options.dataCollection.setUserInfo(false) + givenAttributes( + mapOf( + HttpAttributes.HTTP_REQUEST_METHOD to "GET", + UrlAttributes.URL_QUERY to "name=value&token=secret", + ) + ) + + whenExtractingAttributes() + + thenQueryIsSetTo("name=value&token=[Filtered]") + } + + @Test + fun `data collection can disable URL query attributes`() { + fixture.options.dataCollection.queryParams = KeyValueCollectionBehavior.off() + givenAttributes( + mapOf( + HttpAttributes.HTTP_REQUEST_METHOD to "GET", + UrlAttributes.URL_QUERY to "name=value", + ) + ) + + whenExtractingAttributes() + + assertNull(fixture.scope.request!!.queryString) + } + @Test fun `when there is an existing request on scope it is filled with more details`() { fixture.scope.request = Request().also { it.bodySize = 123L } diff --git a/sentry-servlet-jakarta/src/main/java/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessor.java b/sentry-servlet-jakarta/src/main/java/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessor.java index 777dd13c037..1904d2e5cf0 100644 --- a/sentry-servlet-jakarta/src/main/java/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessor.java +++ b/sentry-servlet-jakarta/src/main/java/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessor.java @@ -36,9 +36,11 @@ public SentryRequestHttpServletRequestProcessor( final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); final @NotNull UrlUtils.UrlDetails urlDetails = - UrlUtils.parse(httpRequest.getRequestURL().toString()); + UrlUtils.parse(httpRequest.getRequestURL().toString(), options.getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); - sentryRequest.setQueryString(httpRequest.getQueryString()); + sentryRequest.setQueryString( + UrlUtils.filterQueryParams( + httpRequest.getQueryString(), options.getDataCollectionResolver())); sentryRequest.setHeaders(resolveHeadersMap(httpRequest)); event.setRequest(sentryRequest); diff --git a/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java b/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java index 2034ab3c75d..789ed1b766f 100644 --- a/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java +++ b/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java @@ -36,9 +36,11 @@ public SentryRequestHttpServletRequestProcessor( final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); final @NotNull UrlUtils.UrlDetails urlDetails = - UrlUtils.parse(httpRequest.getRequestURL().toString()); + UrlUtils.parse(httpRequest.getRequestURL().toString(), options.getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); - sentryRequest.setQueryString(httpRequest.getQueryString()); + sentryRequest.setQueryString( + UrlUtils.filterQueryParams( + httpRequest.getQueryString(), options.getDataCollectionResolver())); sentryRequest.setHeaders(resolveHeadersMap(httpRequest)); event.setRequest(sentryRequest); diff --git a/sentry-servlet/src/test/kotlin/io/sentry/servlet/SentryRequestHttpServletRequestProcessorTest.kt b/sentry-servlet/src/test/kotlin/io/sentry/servlet/SentryRequestHttpServletRequestProcessorTest.kt index 48be73bdc53..f6bd09894a8 100644 --- a/sentry-servlet/src/test/kotlin/io/sentry/servlet/SentryRequestHttpServletRequestProcessorTest.kt +++ b/sentry-servlet/src/test/kotlin/io/sentry/servlet/SentryRequestHttpServletRequestProcessorTest.kt @@ -38,6 +38,33 @@ class SentryRequestHttpServletRequestProcessorTest { assertEquals("param1=xyz", eventRequest.queryString) } + @Test + fun `data collection filters query parameters`() { + val request = + MockMvcRequestBuilders.get(URI.create("http://example.com?name=value&token=secret")) + .buildRequest(MockServletContext()) + val options = SentryOptions().also { it.dataCollection.setUserInfo(false) } + val event = SentryEvent() + + SentryRequestHttpServletRequestProcessor(request, options).process(event, Hint()) + + assertEquals("name=value&token=[Filtered]", event.request!!.queryString) + } + + @Test + fun `data collection can disable query parameters`() { + val request = + MockMvcRequestBuilders.get(URI.create("http://example.com?name=value")) + .buildRequest(MockServletContext()) + val options = + SentryOptions().also { it.dataCollection.queryParams = KeyValueCollectionBehavior.off() } + val event = SentryEvent() + + SentryRequestHttpServletRequestProcessor(request, options).process(event, Hint()) + + assertNull(event.request!!.queryString) + } + @Test fun `attaches header with multiple values`() { val request = diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java index abc809933b0..0f1ef3fcfff 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java @@ -37,9 +37,13 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); final @NotNull UrlUtils.UrlDetails urlDetails = - UrlUtils.parse(httpRequest.getRequestURL().toString()); + UrlUtils.parse( + httpRequest.getRequestURL().toString(), + scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); - sentryRequest.setQueryString(httpRequest.getQueryString()); + sentryRequest.setQueryString( + UrlUtils.filterQueryParams( + httpRequest.getQueryString(), scopes.getOptions().getDataCollectionResolver())); final @NotNull List additionalSecurityCookieNames = extractSecurityCookieNamesOrUseCached(httpRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest, additionalSecurityCookieNames)); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java index 50a8d0539b0..46a31245ba1 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -63,7 +63,9 @@ public SentrySpanClientHttpRequestInterceptor( final ISpan span = activeSpan.startChild("http.client", null, spanOptions); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse( + request.getURI().toString(), scopes.getOptions().getDataCollectionResolver()); span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); urlDetails.applyToSpan(span); @@ -135,7 +137,11 @@ private void addBreadcrumb( final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; final Breadcrumb breadcrumb = - Breadcrumb.http(request.getURI().toString(), methodName, responseStatusCode); + Breadcrumb.http( + request.getURI().toString(), + methodName, + responseStatusCode, + scopes.getOptions().getDataCollectionResolver()); breadcrumb.setData("request_body_size", body.length); final Hint hint = new Hint(); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java index 6726302a83e..ae2446f121f 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java @@ -15,6 +15,7 @@ import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; +import io.sentry.util.UrlUtils; import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,9 +46,19 @@ public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { final @NotNull SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(request.url().toString(), scopes.getOptions().getDataCollectionResolver()); final @NotNull String method = request.method().name(); - span.setDescription(method + " " + request.url()); + span.setDescription( + method + + " " + + (scopes.getOptions().getDataCollectionResolver().isDataCollectionConfigured() + ? urlDetails.getUrlOrFallback() + : request.url())); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); + if (scopes.getOptions().getDataCollectionResolver().isDataCollectionConfigured()) { + urlDetails.applyToSpan(span); + } final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); @@ -113,7 +124,8 @@ private void addBreadcrumb( Breadcrumb.http( request.url().toString(), request.method().name(), - response != null ? response.statusCode().value() : null); + response != null ? response.statusCode().value() : null, + scopes.getOptions().getDataCollectionResolver()); final Hint hint = new Hint(); hint.set(SPRING_EXCHANGE_FILTER_REQUEST, request); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java index 0b41974a69d..4dd05110bbf 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java @@ -96,7 +96,13 @@ protected void doFirst( hint.set(WEBFLUX_FILTER_RESPONSE, response); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - requestScopes.addBreadcrumb(Breadcrumb.http(request.getURI().toString(), methodName), hint); + final @NotNull Breadcrumb breadcrumb = + Breadcrumb.http( + request.getURI().toString(), + methodName, + null, + requestScopes.getOptions().getDataCollectionResolver()); + requestScopes.addBreadcrumb(breadcrumb, hint); requestScopes.configureScope( scope -> scope.setRequest(sentryRequestResolver.resolveSentryRequest(request))); } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java index 229ab887665..a355d379a83 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java @@ -32,7 +32,8 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; sentryRequest.setMethod(methodName); final @NotNull URI uri = httpRequest.getURI(); - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(uri.toString(), scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt index bb14538d921..c6c65b560db 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt @@ -270,7 +270,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes, times(2)).isEnabled - verify(fixture.scopes, times(4)).options + verify(fixture.scopes, times(5)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java index 857027f70d3..81f053f32a1 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java @@ -37,9 +37,13 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); final @NotNull UrlUtils.UrlDetails urlDetails = - UrlUtils.parse(httpRequest.getRequestURL().toString()); + UrlUtils.parse( + httpRequest.getRequestURL().toString(), + scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); - sentryRequest.setQueryString(httpRequest.getQueryString()); + sentryRequest.setQueryString( + UrlUtils.filterQueryParams( + httpRequest.getQueryString(), scopes.getOptions().getDataCollectionResolver())); final @NotNull List additionalSecurityCookieNames = extractSecurityCookieNamesOrUseCached(httpRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest, additionalSecurityCookieNames)); diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java index e305816bb05..0628bc1d30e 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -63,7 +63,9 @@ public SentrySpanClientHttpRequestInterceptor( final ISpan span = activeSpan.startChild("http.client", null, spanOptions); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse( + request.getURI().toString(), scopes.getOptions().getDataCollectionResolver()); span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); urlDetails.applyToSpan(span); @@ -135,7 +137,11 @@ private void addBreadcrumb( final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; final Breadcrumb breadcrumb = - Breadcrumb.http(request.getURI().toString(), methodName, responseStatusCode); + Breadcrumb.http( + request.getURI().toString(), + methodName, + responseStatusCode, + scopes.getOptions().getDataCollectionResolver()); breadcrumb.setData("request_body_size", body.length); final Hint hint = new Hint(); diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java index 1189532c0c4..51f68afd3f8 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java @@ -15,6 +15,7 @@ import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; +import io.sentry.util.UrlUtils; import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,9 +46,19 @@ public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { final @NotNull SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(request.url().toString(), scopes.getOptions().getDataCollectionResolver()); final @NotNull String method = request.method().name(); - span.setDescription(method + " " + request.url()); + span.setDescription( + method + + " " + + (scopes.getOptions().getDataCollectionResolver().isDataCollectionConfigured() + ? urlDetails.getUrlOrFallback() + : request.url())); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); + if (scopes.getOptions().getDataCollectionResolver().isDataCollectionConfigured()) { + urlDetails.applyToSpan(span); + } final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); @@ -113,7 +124,8 @@ private void addBreadcrumb( Breadcrumb.http( request.url().toString(), request.method().name(), - response != null ? response.statusCode().value() : null); + response != null ? response.statusCode().value() : null, + scopes.getOptions().getDataCollectionResolver()); final Hint hint = new Hint(); hint.set(SPRING_EXCHANGE_FILTER_REQUEST, request); diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java index 57b7b86e40f..84af5a708e0 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java @@ -96,7 +96,13 @@ protected void doFirst( hint.set(WEBFLUX_FILTER_RESPONSE, response); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - requestScopes.addBreadcrumb(Breadcrumb.http(request.getURI().toString(), methodName), hint); + final @NotNull Breadcrumb breadcrumb = + Breadcrumb.http( + request.getURI().toString(), + methodName, + null, + requestScopes.getOptions().getDataCollectionResolver()); + requestScopes.addBreadcrumb(breadcrumb, hint); requestScopes.configureScope( scope -> scope.setRequest(sentryRequestResolver.resolveSentryRequest(request))); } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java index a78a329729b..8a5cf168aa5 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java @@ -32,7 +32,8 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; sentryRequest.setMethod(methodName); final @NotNull URI uri = httpRequest.getURI(); - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(uri.toString(), scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt index f0b8d62e025..0a01b4cbcc4 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt @@ -270,7 +270,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes, times(2)).isEnabled - verify(fixture.scopes, times(4)).options + verify(fixture.scopes, times(5)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java b/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java index 6e71d22b902..b33f51e41a2 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java @@ -37,9 +37,13 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); final @NotNull UrlUtils.UrlDetails urlDetails = - UrlUtils.parse(httpRequest.getRequestURL().toString()); + UrlUtils.parse( + httpRequest.getRequestURL().toString(), + scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); - sentryRequest.setQueryString(httpRequest.getQueryString()); + sentryRequest.setQueryString( + UrlUtils.filterQueryParams( + httpRequest.getQueryString(), scopes.getOptions().getDataCollectionResolver())); final @NotNull List additionalSecurityCookieNames = extractSecurityCookieNamesOrUseCached(httpRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest, additionalSecurityCookieNames)); diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java index ed63c5ea080..3a0bd6fc8cb 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -55,7 +55,9 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IScopes scopes) { final ISpan span = activeSpan.startChild("http.client", null, spanOptions); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse( + request.getURI().toString(), scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToSpan(span); span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); @@ -127,7 +129,11 @@ private void addBreadcrumb( final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; final Breadcrumb breadcrumb = - Breadcrumb.http(request.getURI().toString(), methodName, responseStatusCode); + Breadcrumb.http( + request.getURI().toString(), + methodName, + responseStatusCode, + scopes.getOptions().getDataCollectionResolver()); breadcrumb.setData("request_body_size", body.length); final Hint hint = new Hint(); diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java index e9d787a3dec..eda50c41af2 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java @@ -45,7 +45,8 @@ public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { final @NotNull SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild("http.client", null, spanOptions); - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url().toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(request.url().toString(), scopes.getOptions().getDataCollectionResolver()); final @NotNull String method = request.method().name(); span.setDescription(method + " " + urlDetails.getUrlOrFallback()); span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); @@ -115,7 +116,8 @@ private void addBreadcrumb( Breadcrumb.http( request.url().toString(), request.method().name(), - response != null ? response.statusCode().value() : null); + response != null ? response.statusCode().value() : null, + scopes.getOptions().getDataCollectionResolver()); final Hint hint = new Hint(); hint.set(SPRING_EXCHANGE_FILTER_REQUEST, request); diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java index 5e0c1a9b724..7c6ecfb5ad0 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java @@ -32,7 +32,8 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; sentryRequest.setMethod(methodName); final @NotNull URI uri = httpRequest.getURI(); - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(uri.toString(), scopes.getOptions().getDataCollectionResolver()); urlDetails.applyToRequest(sentryRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java index 03333d95417..30d1152b88a 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java @@ -102,8 +102,13 @@ isTracingEnabled && shouldTraceRequest(requestScopes, request) hint.set(WEBFLUX_FILTER_RESPONSE, response); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - requestScopes.addBreadcrumb( - Breadcrumb.http(request.getURI().toString(), methodName), hint); + final @NotNull Breadcrumb breadcrumb = + Breadcrumb.http( + request.getURI().toString(), + methodName, + null, + requestScopes.getOptions().getDataCollectionResolver()); + requestScopes.addBreadcrumb(breadcrumb, hint); requestScopes.configureScope( scope -> scope.setRequest(sentryRequestResolver.resolveSentryRequest(request))); }); diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt index 5d91ec58486..326b5979991 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt @@ -271,7 +271,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes).isEnabled - verify(fixture.scopes, times(4)).options + verify(fixture.scopes, times(5)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7f686446dd0..d9aa46e379c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -132,6 +132,7 @@ public final class io/sentry/Breadcrumb : io/sentry/JsonSerializable, io/sentry/ public fun hashCode ()I public static fun http (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/Breadcrumb; public static fun http (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/sentry/Breadcrumb; + public static fun http (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Lio/sentry/DataCollectionResolver;)Lio/sentry/Breadcrumb; public static fun info (Ljava/lang/String;)Lio/sentry/Breadcrumb; public static fun navigation (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/Breadcrumb; public static fun query (Ljava/lang/String;)Lio/sentry/Breadcrumb; @@ -7809,6 +7810,7 @@ public final class io/sentry/util/HttpUtils { public static fun filterOutSecurityCookies (Ljava/lang/String;Ljava/util/List;)Ljava/lang/String; public static fun filterOutSecurityCookiesFromHeader (Ljava/util/Enumeration;Ljava/lang/String;Ljava/util/List;)Ljava/util/List; public static fun filterOutSecurityCookiesFromHeader (Ljava/util/List;Ljava/lang/String;Ljava/util/List;)Ljava/util/List; + public static fun filterQueryParams (Ljava/lang/String;Lio/sentry/KeyValueCollectionBehavior;)Ljava/lang/String; public static fun isHttpClientError (I)Z public static fun isHttpServerError (I)Z public static fun isSecurityCookie (Ljava/lang/String;Ljava/util/List;)Z @@ -8067,7 +8069,9 @@ public final class io/sentry/util/UUIDStringUtils { public final class io/sentry/util/UrlUtils { public static final field SENSITIVE_DATA_SUBSTITUTE Ljava/lang/String; public fun ()V + public static fun filterQueryParams (Ljava/lang/String;Lio/sentry/DataCollectionResolver;)Ljava/lang/String; public static fun parse (Ljava/lang/String;)Lio/sentry/util/UrlUtils$UrlDetails; + public static fun parse (Ljava/lang/String;Lio/sentry/DataCollectionResolver;)Lio/sentry/util/UrlUtils$UrlDetails; public static fun parseNullable (Ljava/lang/String;)Lio/sentry/util/UrlUtils$UrlDetails; } diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java index fff6954ee56..b04bddb159a 100644 --- a/sentry/src/main/java/io/sentry/Breadcrumb.java +++ b/sentry/src/main/java/io/sentry/Breadcrumb.java @@ -192,8 +192,15 @@ public static Breadcrumb fromMap( * @return the breadcrumb */ public static @NotNull Breadcrumb http(final @NotNull String url, final @NotNull String method) { + return createHttpBreadcrumb(url, method, null); + } + + private static @NotNull Breadcrumb createHttpBreadcrumb( + final @NotNull String url, + final @NotNull String method, + final @Nullable DataCollectionResolver resolver) { final Breadcrumb breadcrumb = new Breadcrumb(); - final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url, resolver); breadcrumb.setType("http"); breadcrumb.setCategory("http"); if (urlDetails.getUrl() != null) { @@ -220,7 +227,21 @@ public static Breadcrumb fromMap( */ public static @NotNull Breadcrumb http( final @NotNull String url, final @NotNull String method, final @Nullable Integer code) { - final Breadcrumb breadcrumb = http(url, method); + final Breadcrumb breadcrumb = createHttpBreadcrumb(url, method, null); + if (code != null) { + breadcrumb.setData("status_code", code); + breadcrumb.setLevel(levelFromHttpStatusCode(code)); + } + return breadcrumb; + } + + @ApiStatus.Internal + public static @NotNull Breadcrumb http( + final @NotNull String url, + final @NotNull String method, + final @Nullable Integer code, + final @Nullable DataCollectionResolver resolver) { + final Breadcrumb breadcrumb = createHttpBreadcrumb(url, method, resolver); if (code != null) { breadcrumb.setData("status_code", code); breadcrumb.setLevel(levelFromHttpStatusCode(code)); diff --git a/sentry/src/main/java/io/sentry/util/HttpUtils.java b/sentry/src/main/java/io/sentry/util/HttpUtils.java index d6b9072284e..936571f4ba7 100644 --- a/sentry/src/main/java/io/sentry/util/HttpUtils.java +++ b/sentry/src/main/java/io/sentry/util/HttpUtils.java @@ -4,6 +4,7 @@ import io.sentry.HttpStatusCodeRange; import io.sentry.KeyValueCollectionBehavior; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -76,6 +77,40 @@ public static boolean containsSensitiveHeader(final @NotNull String header) { return SENSITIVE_HEADERS.contains(header.toUpperCase(Locale.ROOT)); } + public static @Nullable String filterQueryParams( + final @Nullable String query, final @NotNull KeyValueCollectionBehavior behavior) { + if (query == null || behavior.getMode() == KeyValueCollectionBehavior.Mode.OFF) { + return null; + } + + final @NotNull StringBuilder filteredQuery = new StringBuilder(); + final @NotNull String[] params = query.split("&", -1); + for (int i = 0; i < params.length; i++) { + if (i > 0) { + filteredQuery.append('&'); + } + + final @NotNull String param = params[i]; + final int separator = param.indexOf('='); + final @NotNull String name = separator < 0 ? param : param.substring(0, separator); + final @NotNull String decodedName = decodeQueryParamName(name); + final boolean sensitive = containsTerm(decodedName, SENSITIVE_DATA_KEYS); + final boolean matchesTerm = containsTerm(decodedName, behavior.getTerms()); + final boolean shouldFilter = + sensitive + || (behavior.getMode() == KeyValueCollectionBehavior.Mode.DENY_LIST && matchesTerm) + || (behavior.getMode() == KeyValueCollectionBehavior.Mode.ALLOW_LIST && !matchesTerm); + + filteredQuery.append(name); + if (shouldFilter) { + filteredQuery.append('=').append(SENSITIVE_DATA_SUBSTITUTE); + } else if (separator >= 0) { + filteredQuery.append(param.substring(separator)); + } + } + return filteredQuery.toString(); + } + public static @NotNull Map filterHeaders( final @NotNull Map headers, final @NotNull KeyValueCollectionBehavior behavior) { @@ -104,6 +139,14 @@ public static boolean containsSensitiveHeader(final @NotNull String header) { return filteredHeaders; } + private static @NotNull String decodeQueryParamName(final @NotNull String name) { + try { + return URLDecoder.decode(name, "UTF-8"); + } catch (Throwable ignored) { + return name; + } + } + private static boolean containsTerm( final @NotNull String key, final @NotNull List terms) { final @NotNull String normalizedKey = key.toLowerCase(Locale.ROOT); diff --git a/sentry/src/main/java/io/sentry/util/UrlUtils.java b/sentry/src/main/java/io/sentry/util/UrlUtils.java index 6c70cea0495..6dc33795b1d 100644 --- a/sentry/src/main/java/io/sentry/util/UrlUtils.java +++ b/sentry/src/main/java/io/sentry/util/UrlUtils.java @@ -1,5 +1,6 @@ package io.sentry.util; +import io.sentry.DataCollectionResolver; import io.sentry.ISpan; import io.sentry.SpanDataConvention; import io.sentry.protocol.Request; @@ -18,6 +19,11 @@ public final class UrlUtils { } public static @NotNull UrlDetails parse(final @NotNull String url) { + return parse(url, null); + } + + public static @NotNull UrlDetails parse( + final @NotNull String url, final @Nullable DataCollectionResolver resolver) { try { URI uri = new URI(url); if (uri.isAbsolute() && !isValidAbsoluteUrl(uri)) { @@ -28,7 +34,9 @@ public final class UrlUtils { uri.getScheme() == null ? "" : (uri.getScheme() + "://"); final @NotNull String authority = uri.getRawAuthority() == null ? "" : uri.getRawAuthority(); final @NotNull String path = uri.getRawPath() == null ? "" : uri.getRawPath(); - final @Nullable String query = uri.getRawQuery(); + final @Nullable String rawQuery = uri.getRawQuery(); + final @Nullable String query = + resolver == null ? rawQuery : filterQueryParams(rawQuery, resolver); final @Nullable String fragment = uri.getRawFragment(); final @NotNull String filteredUrl = schemeAndSeparator + filterUserInfo(authority) + path; @@ -39,6 +47,13 @@ public final class UrlUtils { } } + public static @Nullable String filterQueryParams( + final @Nullable String query, final @NotNull DataCollectionResolver resolver) { + return resolver.isDataCollectionConfigured() + ? HttpUtils.filterQueryParams(query, resolver.getQueryParams()) + : query; + } + private static boolean isValidAbsoluteUrl(final @NotNull URI uri) { try { uri.toURL(); diff --git a/sentry/src/test/java/io/sentry/util/HttpUtilsTest.kt b/sentry/src/test/java/io/sentry/util/HttpUtilsTest.kt index 1e9ed10f806..1da3b82b516 100644 --- a/sentry/src/test/java/io/sentry/util/HttpUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/HttpUtilsTest.kt @@ -10,6 +10,55 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class HttpUtilsTest { + @Test + fun `query parameter filter disables collection in off mode`() { + assertThat(HttpUtils.filterQueryParams("name=value", KeyValueCollectionBehavior.off())).isNull() + } + + @Test + fun `query parameter deny list filters built-in sensitive and configured terms`() { + assertThat( + HttpUtils.filterQueryParams( + "name=value&access_token=secret&customerId=123", + KeyValueCollectionBehavior.denyList("customer"), + ) + ) + .isEqualTo("name=value&access_token=[Filtered]&customerId=[Filtered]") + } + + @Test + fun `query parameter allow list only retains allowed non-sensitive values`() { + assertThat( + HttpUtils.filterQueryParams( + "name=value&access_token=secret&customerId=123", + KeyValueCollectionBehavior.allowList("name", "access_token"), + ) + ) + .isEqualTo("name=value&access_token=[Filtered]&customerId=[Filtered]") + } + + @Test + fun `query parameter filter matches decoded names and preserves encoding`() { + assertThat( + HttpUtils.filterQueryParams( + "access%5Ftoken=secret&display%20name=Jane+Doe", + KeyValueCollectionBehavior.denyList(), + ) + ) + .isEqualTo("access%5Ftoken=[Filtered]&display%20name=Jane+Doe") + } + + @Test + fun `query parameter filter preserves empty parameters and values`() { + assertThat( + HttpUtils.filterQueryParams( + "name=&flag&&token", + KeyValueCollectionBehavior.denyList(), + ) + ) + .isEqualTo("name=&flag&&token=[Filtered]") + } + @Test fun `header filter disables collection in off mode`() { val filtered = diff --git a/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt b/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt index a971fbf7d71..91065f6e50e 100644 --- a/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt @@ -1,10 +1,76 @@ package io.sentry.util +import com.google.common.truth.Truth.assertThat +import io.sentry.Breadcrumb +import io.sentry.ISpan +import io.sentry.KeyValueCollectionBehavior +import io.sentry.SentryOptions +import io.sentry.SpanDataConvention +import io.sentry.protocol.Request import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify class UrlUtilsTest { + @Test + fun `resolver aware helpers preserve legacy query values`() { + val resolver = SentryOptions().dataCollectionResolver + val details = UrlUtils.parse("https://example.com?token=secret", resolver) + val request = Request() + + details.applyToRequest(request) + + assertThat(request.queryString).isEqualTo("token=secret") + } + + @Test + fun `resolver aware helpers filter request span and breadcrumb queries`() { + val options = SentryOptions().also { it.dataCollection.setUserInfo(false) } + val details = + UrlUtils.parse( + "https://example.com?name=value&token=secret", + options.dataCollectionResolver, + ) + val request = Request() + val span = mock() + val breadcrumb = + Breadcrumb.http( + "https://example.com?name=value&token=secret", + "GET", + null, + options.dataCollectionResolver, + ) + + details.applyToRequest(request) + details.applyToSpan(span) + + assertThat(request.queryString).isEqualTo("name=value&token=[Filtered]") + verify(span).setData(SpanDataConvention.HTTP_QUERY_KEY, "name=value&token=[Filtered]") + assertThat(breadcrumb.getData("http.query")).isEqualTo("name=value&token=[Filtered]") + } + + @Test + fun `resolver aware helpers remove query values in off mode`() { + val options = + SentryOptions().also { it.dataCollection.queryParams = KeyValueCollectionBehavior.off() } + val details = UrlUtils.parse("https://example.com?name=value", options.dataCollectionResolver) + val request = Request() + val breadcrumb = + Breadcrumb.http( + "https://example.com?name=value", + "GET", + null, + options.dataCollectionResolver, + ) + + details.applyToRequest(request) + + assertThat(request.queryString).isNull() + assertThat(breadcrumb.getData("http.query")).isNull() + } + @Test fun `returns null for null`() { assertNull(UrlUtils.parseNullable(null))