diff --git a/CHANGELOG.md b/CHANGELOG.md index d335ce1acb..c7c0c07475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Remove authority from URLs sent to Sentry ([#2366](https://github.com/getsentry/sentry-java/pull/2366)) + ### Dependencies - Bump Native SDK from v0.5.3 to v0.5.4 ([#2500](https://github.com/getsentry/sentry-java/pull/2500)) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index 5cb9da2183..0e28e6f460 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -16,6 +16,7 @@ import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.HttpUtils import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.UrlUtils import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Request @@ -53,11 +54,13 @@ class SentryOkHttpInterceptor( override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - val url = request.url.toString() + val urlDetails = UrlUtils.parse(request.url.toString()) + val url = urlDetails.urlOrFallback val method = request.method // read transaction from the bound scope val span = hub.span?.startChild("http.client", "$method $url") + urlDetails.applyToSpan(span) var response: Response? = null @@ -149,20 +152,10 @@ class SentryOkHttpInterceptor( // 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 - var requestUrl = request.url.toString() - - val query = request.url.query - if (!query.isNullOrEmpty()) { - requestUrl = requestUrl.replace("?$query", "") - } - - val urlFragment = request.url.fragment - if (!urlFragment.isNullOrEmpty()) { - requestUrl = requestUrl.replace("#$urlFragment", "") - } + val urlDetails = UrlUtils.parse(request.url.toString()) // return if its not a target match - if (!PropagationTargetsUtils.contain(failedRequestTargets, requestUrl)) { + if (!PropagationTargetsUtils.contain(failedRequestTargets, request.url.toString())) { return } @@ -180,13 +173,11 @@ class SentryOkHttpInterceptor( hint.set(OKHTTP_RESPONSE, response) val sentryRequest = io.sentry.protocol.Request().apply { - url = requestUrl + urlDetails.applyToRequest(this) // Cookie is only sent if isSendDefaultPii is enabled cookies = if (hub.options.isSendDefaultPii) request.headers["Cookie"] else null method = request.method - queryString = query headers = getHeaders(request.headers) - fragment = urlFragment request.body?.contentLength().ifHasValidLength { bodySize = it diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt index 9429a2112f..eed10ea8c6 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt @@ -263,6 +263,8 @@ class SentryOkHttpInterceptorTest { @SuppressWarnings("SwallowedException") @Test fun `adds breadcrumb when http calls results in exception`() { + // to setup mocks + fixture.getSut() val interceptor = SentryOkHttpInterceptor(fixture.hub) val chain = mock() whenever(chain.proceed(any())).thenThrow(IOException()) @@ -408,7 +410,8 @@ class SentryOkHttpInterceptorTest { val sut = fixture.getSut( captureFailedRequests = true, httpStatusCode = statusCode, - responseBody = "fail" + responseBody = "fail", + sendDefaultPii = true ) val request = getRequest(url = "/hello?myQuery=myValue#myFragment") @@ -423,7 +426,7 @@ class SentryOkHttpInterceptorTest { assertEquals("GET", sentryRequest.method) // because of isSendDefaultPii - assertNull(sentryRequest.headers) + assertNotNull(sentryRequest.headers) assertNull(sentryRequest.cookies) val sentryResponse = it.contexts.response!! @@ -431,8 +434,8 @@ class SentryOkHttpInterceptorTest { assertEquals(response.body!!.contentLength(), sentryResponse.bodySize) // because of isSendDefaultPii - assertNull(sentryRequest.headers) - assertNull(sentryRequest.cookies) + assertNotNull(sentryResponse.headers) + assertNull(sentryResponse.cookies) assertTrue(it.throwable is SentryHttpClientException) }, @@ -489,6 +492,8 @@ class SentryOkHttpInterceptorTest { @SuppressWarnings("SwallowedException") @Test fun `does not capture an error even if it throws`() { + // to setup mocks + fixture.getSut() val interceptor = SentryOkHttpInterceptor( fixture.hub, captureFailedRequests = true 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 a3615d3289..71b8c5adcb 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 @@ -17,6 +17,7 @@ import io.sentry.SentryLevel import io.sentry.SpanStatus import io.sentry.TypeCheckHint import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.UrlUtils class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null) : HttpInterceptor { @@ -80,7 +81,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH } private fun startChild(request: HttpRequest, activeSpan: ISpan): ISpan { - val url = request.url + val urlDetails = UrlUtils.parse(request.url) val method = request.method val operationName = operationNameFromHeaders(request) @@ -88,9 +89,11 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH val operationType = request.valueForHeader(SENTRY_APOLLO_3_OPERATION_TYPE) ?: method val operationId = request.valueForHeader("X-APOLLO-OPERATION-ID") val variables = request.valueForHeader(SENTRY_APOLLO_3_VARIABLES) - val description = "$operationType ${operationName ?: url}" + val description = "$operationType ${operationName ?: urlDetails.urlOrFallback}" return activeSpan.startChild(operation, description).apply { + urlDetails.applyToSpan(this) + operationId?.let { setData("operationId", it) } @@ -121,8 +124,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH } span.finish() - val breadcrumb = - Breadcrumb.http(request.url, request.method.name, statusCode) + val breadcrumb = Breadcrumb.http(request.url, request.method.name, statusCode) request.body?.contentLength.ifHasValidLength { contentLength -> breadcrumb.setData("request_body_size", contentLength) 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 c25feb57fa..ead779c3e8 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -15,6 +15,7 @@ import io.sentry.SpanStatus; import io.sentry.util.Objects; import io.sentry.util.PropagationTargetsUtils; +import io.sentry.util.UrlUtils; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -50,13 +51,15 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O } ISpan span = activeSpan.startChild("http.client"); - String url = request.url(); - span.setDescription(request.httpMethod().name() + " " + url); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url()); + span.setDescription(request.httpMethod().name() + " " + urlDetails.getUrlOrFallback()); + urlDetails.applyToSpan(span); final RequestWrapper requestWrapper = new RequestWrapper(request); if (!span.isNoOp() - && PropagationTargetsUtils.contain(hub.getOptions().getTracePropagationTargets(), url)) { + && PropagationTargetsUtils.contain( + hub.getOptions().getTracePropagationTargets(), request.url())) { final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); final @Nullable Collection requestBaggageHeader = request.headers().get(BaggageHeader.BAGGAGE_HEADER); 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 20c97a0710..17ac102090 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 @@ -6,6 +6,7 @@ import io.sentry.protocol.Request; import io.sentry.util.HttpUtils; import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; import jakarta.servlet.http.HttpServletRequest; import java.util.Collections; import java.util.Enumeration; @@ -30,8 +31,10 @@ public SentryRequestHttpServletRequestProcessor(@NotNull HttpServletRequest http public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(httpRequest.getRequestURL().toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setQueryString(httpRequest.getQueryString()); - sentryRequest.setUrl(httpRequest.getRequestURL().toString()); 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 b11f17e739..b24c0446b0 100644 --- a/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java +++ b/sentry-servlet/src/main/java/io/sentry/servlet/SentryRequestHttpServletRequestProcessor.java @@ -6,6 +6,7 @@ import io.sentry.protocol.Request; import io.sentry.util.HttpUtils; import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -30,8 +31,10 @@ public SentryRequestHttpServletRequestProcessor(@NotNull HttpServletRequest http public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(httpRequest.getRequestURL().toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setQueryString(httpRequest.getQueryString()); - sentryRequest.setUrl(httpRequest.getRequestURL().toString()); sentryRequest.setHeaders(resolveHeadersMap(httpRequest)); event.setRequest(sentryRequest); 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 e4674609a6..eda5d2e537 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 @@ -9,6 +9,8 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; + +import io.sentry.util.UrlUtils; import jakarta.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,8 +28,9 @@ public SentryRequestResolver(final @NotNull IHub hub) { public @NotNull Request resolveSentryRequest(final @NotNull HttpServletRequest httpRequest) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(httpRequest.getRequestURL().toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setQueryString(httpRequest.getQueryString()); - sentryRequest.setUrl(httpRequest.getRequestURL().toString()); sentryRequest.setHeaders(resolveHeadersMap(httpRequest)); if (hub.getOptions().isSendDefaultPii()) { 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 b3cf87bf2e..e51c3e0b20 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 @@ -14,6 +14,8 @@ import io.sentry.SpanStatus; import io.sentry.util.Objects; import io.sentry.util.PropagationTargetsUtils; +import io.sentry.util.UrlUtils; + import java.io.IOException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,7 +49,9 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - span.setDescription(methodName + " " + request.getURI()); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); + urlDetails.applyToSpan(span); if (!span.isNoOp() && PropagationTargetsUtils.contain( hub.getOptions().getTracePropagationTargets(), request.getURI())) { 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 d0590b159d..02701ab957 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 @@ -5,6 +5,9 @@ import io.sentry.protocol.Request; import io.sentry.util.HttpUtils; import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; + +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,8 +31,9 @@ public SentryRequestResolver(final @NotNull IHub hub) { final String methodName = httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; sentryRequest.setMethod(methodName); - sentryRequest.setQueryString(httpRequest.getURI().getQuery()); - sentryRequest.setUrl(httpRequest.getURI().toString()); + final @NotNull URI uri = httpRequest.getURI(); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); if (hub.getOptions().isSendDefaultPii()) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt index 8b9710060b..2abb45b913 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt @@ -38,6 +38,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull @RunWith(SpringRunner::class) @SpringBootTest( @@ -63,7 +64,7 @@ class SentryWebfluxIntegrationTest { @Test fun `attaches request information to SentryEvents`() { testClient.get() - .uri("http://localhost:$port/hello?param=value") + .uri("http://localhost:$port/hello?param=value#top") .exchange() .expectStatus() .isOk @@ -71,9 +72,10 @@ class SentryWebfluxIntegrationTest { verify(transport).send( checkEvent { event -> assertNotNull(event.request) { - assertEquals("http://localhost:$port/hello?param=value", it.url) + assertEquals("http://localhost:$port/hello", it.url) assertEquals("GET", it.method) assertEquals("param=value", it.queryString) + assertNull(it.fragment) } }, anyOrNull() 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 18aa9bcded..59dc4def54 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java @@ -5,6 +5,7 @@ import io.sentry.protocol.Request; import io.sentry.util.HttpUtils; import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -26,8 +27,10 @@ public SentryRequestResolver(final @NotNull IHub hub) { public @NotNull Request resolveSentryRequest(final @NotNull HttpServletRequest httpRequest) { final Request sentryRequest = new Request(); sentryRequest.setMethod(httpRequest.getMethod()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(httpRequest.getRequestURL().toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setQueryString(httpRequest.getQueryString()); - sentryRequest.setUrl(httpRequest.getRequestURL().toString()); sentryRequest.setHeaders(resolveHeadersMap(httpRequest)); if (hub.getOptions().isSendDefaultPii()) { 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 9a445fb7e3..09a4bcc8a0 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 @@ -14,6 +14,7 @@ import io.sentry.SpanStatus; import io.sentry.util.Objects; import io.sentry.util.PropagationTargetsUtils; +import io.sentry.util.UrlUtils; import java.io.IOException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -47,7 +48,9 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; - span.setDescription(methodName + " " + request.getURI()); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + urlDetails.applyToSpan(span); + span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); if (!span.isNoOp() && PropagationTargetsUtils.contain( 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 399e7044c9..1c82865ed7 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 @@ -13,6 +13,7 @@ import io.sentry.SpanStatus; import io.sentry.util.Objects; import io.sentry.util.PropagationTargetsUtils; +import io.sentry.util.UrlUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.web.reactive.function.client.ClientRequest; @@ -39,7 +40,9 @@ public SentrySpanClientWebRequestFilter(final @NotNull IHub hub) { } final ISpan span = activeSpan.startChild("http.client"); - span.setDescription(request.method().name() + " " + request.url()); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url().toString()); + span.setDescription(request.method().name() + " " + urlDetails.getUrlOrFallback()); + urlDetails.applyToSpan(span); final ClientRequest.Builder requestBuilder = ClientRequest.from(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 3c66dae6ea..dd2006466a 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 @@ -5,6 +5,8 @@ import io.sentry.protocol.Request; import io.sentry.util.HttpUtils; import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,8 +30,9 @@ public SentryRequestResolver(final @NotNull IHub hub) { final String methodName = httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; sentryRequest.setMethod(methodName); - sentryRequest.setQueryString(httpRequest.getURI().getQuery()); - sentryRequest.setUrl(httpRequest.getURI().toString()); + final @NotNull URI uri = httpRequest.getURI(); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + urlDetails.applyToRequest(sentryRequest); sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); if (hub.getOptions().isSendDefaultPii()) { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt index 4e125e97c8..1ef8cc0f2a 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt @@ -38,6 +38,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull @RunWith(SpringRunner::class) @SpringBootTest( @@ -63,7 +64,7 @@ class SentryWebfluxIntegrationTest { @Test fun `attaches request information to SentryEvents`() { testClient.get() - .uri("http://localhost:$port/hello?param=value") + .uri("http://localhost:$port/hello?param=value#top") .exchange() .expectStatus() .isOk @@ -71,9 +72,10 @@ class SentryWebfluxIntegrationTest { verify(transport).send( checkEvent { event -> assertNotNull(event.request) { - assertEquals("http://localhost:$port/hello?param=value", it.url) + assertEquals("http://localhost:$port/hello", it.url) assertEquals("GET", it.method) assertEquals("param=value", it.queryString) + assertNull(it.fragment) } }, anyOrNull() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 81bbb428ae..bd4e9ef9ff 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3744,6 +3744,22 @@ public final class io/sentry/util/StringUtils { public static fun removeSurrounding (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; } +public final class io/sentry/util/UrlUtils { + public fun ()V + public static fun parse (Ljava/lang/String;)Lio/sentry/util/UrlUtils$UrlDetails; + public static fun parseNullable (Ljava/lang/String;)Lio/sentry/util/UrlUtils$UrlDetails; +} + +public final class io/sentry/util/UrlUtils$UrlDetails { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun applyToRequest (Lio/sentry/protocol/Request;)V + public fun applyToSpan (Lio/sentry/ISpan;)V + public fun getFragment ()Ljava/lang/String; + public fun getQuery ()Ljava/lang/String; + public fun getUrl ()Ljava/lang/String; + public fun getUrlOrFallback ()Ljava/lang/String; +} + public abstract interface class io/sentry/util/thread/IMainThreadChecker { public fun isMainThread ()Z public abstract fun isMainThread (J)Z diff --git a/sentry/src/main/java/io/sentry/Breadcrumb.java b/sentry/src/main/java/io/sentry/Breadcrumb.java index d3924cb6e1..868360c664 100644 --- a/sentry/src/main/java/io/sentry/Breadcrumb.java +++ b/sentry/src/main/java/io/sentry/Breadcrumb.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.util.CollectionUtils; +import io.sentry.util.UrlUtils; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Collections; @@ -67,10 +68,19 @@ public Breadcrumb(final @NotNull Date timestamp) { */ public static @NotNull Breadcrumb http(final @NotNull String url, final @NotNull String method) { final Breadcrumb breadcrumb = new Breadcrumb(); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(url); breadcrumb.setType("http"); breadcrumb.setCategory("http"); - breadcrumb.setData("url", url); + if (urlDetails.getUrl() != null) { + breadcrumb.setData("url", urlDetails.getUrl()); + } breadcrumb.setData("method", method.toUpperCase(Locale.ROOT)); + if (urlDetails.getQuery() != null) { + breadcrumb.setData("http.query", urlDetails.getQuery()); + } + if (urlDetails.getFragment() != null) { + breadcrumb.setData("http.fragment", urlDetails.getFragment()); + } return breadcrumb; } diff --git a/sentry/src/main/java/io/sentry/util/UrlUtils.java b/sentry/src/main/java/io/sentry/util/UrlUtils.java new file mode 100644 index 0000000000..6cdc4c2d6a --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/UrlUtils.java @@ -0,0 +1,188 @@ +package io.sentry.util; + +import io.sentry.ISpan; +import io.sentry.protocol.Request; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class UrlUtils { + + private static final @NotNull Pattern AUTH_REGEX = Pattern.compile("(.+://)(.*@)(.*)"); + + public static @Nullable UrlDetails parseNullable(final @Nullable String url) { + if (url == null) { + return null; + } + + return parse(url); + } + + public static @NotNull UrlDetails parse(final @NotNull String url) { + if (isAbsoluteUrl(url)) { + return splitAbsoluteUrl(url); + } else { + return splitRelativeUrl(url); + } + } + + private static boolean isAbsoluteUrl(@NotNull String url) { + return url.contains("://"); + } + + private static @NotNull UrlDetails splitRelativeUrl(final @NotNull String url) { + final int queryParamSeparatorIndex = url.indexOf("?"); + final int fragmentSeparatorIndex = url.indexOf("#"); + + final @Nullable String baseUrl = + extractBaseUrl(url, queryParamSeparatorIndex, fragmentSeparatorIndex); + final @Nullable String query = + extractQuery(url, queryParamSeparatorIndex, fragmentSeparatorIndex); + final @Nullable String fragment = extractFragment(url, fragmentSeparatorIndex); + + return new UrlDetails(baseUrl, query, fragment); + } + + private static @Nullable String extractBaseUrl( + final @NotNull String url, + final int queryParamSeparatorIndex, + final int fragmentSeparatorIndex) { + if (queryParamSeparatorIndex >= 0) { + return url.substring(0, queryParamSeparatorIndex).trim(); + } else if (fragmentSeparatorIndex >= 0) { + return url.substring(0, fragmentSeparatorIndex).trim(); + } else { + return url; + } + } + + private static @Nullable String extractQuery( + final @NotNull String url, + final int queryParamSeparatorIndex, + final int fragmentSeparatorIndex) { + if (queryParamSeparatorIndex > 0) { + if (fragmentSeparatorIndex > 0 && fragmentSeparatorIndex > queryParamSeparatorIndex) { + return url.substring(queryParamSeparatorIndex + 1, fragmentSeparatorIndex).trim(); + } else { + return url.substring(queryParamSeparatorIndex + 1).trim(); + } + } else { + return null; + } + } + + private static @Nullable String extractFragment( + final @NotNull String url, final int fragmentSeparatorIndex) { + if (fragmentSeparatorIndex > 0) { + return url.substring(fragmentSeparatorIndex + 1).trim(); + } else { + return null; + } + } + + private static @NotNull UrlDetails splitAbsoluteUrl(final @NotNull String url) { + try { + final @NotNull String filteredUrl = urlWithAuthRemoved(url); + final @NotNull URL urlObj = new URL(url); + final @NotNull String baseUrl = baseUrlOnly(filteredUrl); + if (baseUrl.contains("#")) { + // url considered malformed because it has fragment + return new UrlDetails(null, null, null); + } else { + final @Nullable String query = urlObj.getQuery(); + final @Nullable String fragment = urlObj.getRef(); + return new UrlDetails(baseUrl, query, fragment); + } + } catch (MalformedURLException e) { + return new UrlDetails(null, null, null); + } + } + + private static @NotNull String urlWithAuthRemoved(final @NotNull String url) { + final @NotNull Matcher userInfoMatcher = AUTH_REGEX.matcher(url); + if (userInfoMatcher.matches() && userInfoMatcher.groupCount() == 3) { + final @NotNull String userInfoString = userInfoMatcher.group(2); + final @NotNull String replacementString = + userInfoString.contains(":") ? "[Filtered]:[Filtered]@" : "[Filtered]@"; + return userInfoMatcher.group(1) + replacementString + userInfoMatcher.group(3); + } else { + return url; + } + } + + private static @NotNull String baseUrlOnly(final @NotNull String url) { + final int queryParamSeparatorIndex = url.indexOf("?"); + + if (queryParamSeparatorIndex >= 0) { + return url.substring(0, queryParamSeparatorIndex).trim(); + } else { + final int fragmentSeparatorIndex = url.indexOf("#"); + if (fragmentSeparatorIndex >= 0) { + return url.substring(0, fragmentSeparatorIndex).trim(); + } else { + return url; + } + } + } + + public static final class UrlDetails { + private final @Nullable String url; + private final @Nullable String query; + private final @Nullable String fragment; + + public UrlDetails( + final @Nullable String url, final @Nullable String query, final @Nullable String fragment) { + this.url = url; + this.query = query; + this.fragment = fragment; + } + + public @Nullable String getUrl() { + return url; + } + + public @NotNull String getUrlOrFallback() { + if (url == null) { + return "unknown"; + } else { + return url; + } + } + + public @Nullable String getQuery() { + return query; + } + + public @Nullable String getFragment() { + return fragment; + } + + public void applyToRequest(final @Nullable Request request) { + if (request == null) { + return; + } + + request.setUrl(url); + request.setQueryString(query); + request.setFragment(fragment); + } + + public void applyToSpan(final @Nullable ISpan span) { + if (span == null) { + return; + } + + if (query != null) { + span.setData("http.query", query); + } + if (fragment != null) { + span.setData("http.fragment", fragment); + } + } + } +} diff --git a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt index e6b417cbc0..a710e54ec6 100644 --- a/sentry/src/test/java/io/sentry/BreadcrumbTest.kt +++ b/sentry/src/test/java/io/sentry/BreadcrumbTest.kt @@ -101,8 +101,10 @@ class BreadcrumbTest { @Test fun `creates HTTP breadcrumb`() { - val breadcrumb = Breadcrumb.http("http://example.com", "POST") - assertEquals("http://example.com", breadcrumb.data["url"]) + val breadcrumb = Breadcrumb.http("http://example.com/api?q=1#top", "POST") + assertEquals("http://example.com/api", breadcrumb.data["url"]) + assertEquals("q=1", breadcrumb.data["http.query"]) + assertEquals("top", breadcrumb.data["http.fragment"]) assertEquals("POST", breadcrumb.data["method"]) assertEquals("http", breadcrumb.type) assertEquals("http", breadcrumb.category) diff --git a/sentry/src/test/java/io/sentry/UrlDetailsTest.kt b/sentry/src/test/java/io/sentry/UrlDetailsTest.kt new file mode 100644 index 0000000000..bad6df1a46 --- /dev/null +++ b/sentry/src/test/java/io/sentry/UrlDetailsTest.kt @@ -0,0 +1,83 @@ +package io.sentry + +import io.sentry.protocol.Request +import io.sentry.util.UrlUtils +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class UrlDetailsTest { + + @Test + fun `does not crash on null span`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", "q=1", "top") + urlDetails.applyToSpan(null) + } + + @Test + fun `applies query and fragment to span`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", "q=1", "top") + val span = mock() + urlDetails.applyToSpan(span) + + verify(span).setData("http.query", "q=1") + verify(span).setData("http.fragment", "top") + } + + @Test + fun `applies query to span`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", "q=1", null) + val span = mock() + urlDetails.applyToSpan(span) + + verify(span).setData("http.query", "q=1") + verifyNoMoreInteractions(span) + } + + @Test + fun `applies fragment to span`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", null, "top") + val span = mock() + urlDetails.applyToSpan(span) + + verify(span).setData("http.fragment", "top") + verifyNoMoreInteractions(span) + } + + @Test + fun `does not crash on null request`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", "q=1", "top") + urlDetails.applyToRequest(null) + } + + @Test + fun `applies details to request`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", "q=1", "top") + val request = Request() + urlDetails.applyToRequest(request) + + assertEquals("https://sentry.io/api", request.url) + assertEquals("q=1", request.queryString) + assertEquals("top", request.fragment) + } + + @Test + fun `applies details without fragment and url to request`() { + val urlDetails = UrlUtils.UrlDetails("https://sentry.io/api", null, null) + val request = Request() + urlDetails.applyToRequest(request) + + assertEquals("https://sentry.io/api", request.url) + assertNull(request.queryString) + assertNull(request.fragment) + } + + @Test + fun `returns fallback for null URL`() { + val urlDetails = UrlUtils.UrlDetails(null, null, null) + assertEquals("unknown", urlDetails.urlOrFallback) + } +} diff --git a/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt b/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt new file mode 100644 index 0000000000..af037b3344 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/UrlUtilsTest.kt @@ -0,0 +1,193 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class UrlUtilsTest { + + @Test + fun `returns null for null`() { + assertNull(UrlUtils.parseNullable(null)) + } + + @Test + fun `strips user info with user and password from http nullable`() { + val urlDetails = UrlUtils.parseNullable( + "http://user:password@sentry.io?q=1&s=2&token=secret#top" + )!! + assertEquals("http://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `strips user info with user and password from http`() { + val urlDetails = UrlUtils.parse( + "http://user:password@sentry.io?q=1&s=2&token=secret#top" + ) + assertEquals("http://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `strips user info with user and password from https`() { + val urlDetails = UrlUtils.parse( + "https://user:password@sentry.io?q=1&s=2&token=secret#top" + ) + assertEquals("https://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits url`() { + val urlDetails = UrlUtils.parse( + "https://sentry.io?q=1&s=2&token=secret#top" + ) + assertEquals("https://sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits relative url`() { + val urlDetails = UrlUtils.parse( + "/users/1?q=1&s=2&token=secret#top" + ) + assertEquals("/users/1", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits relative root url`() { + val urlDetails = UrlUtils.parse( + "/?q=1&s=2&token=secret#top" + ) + assertEquals("/", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits url with just query and fragment`() { + val urlDetails = UrlUtils.parse( + "/?q=1&s=2&token=secret#top" + ) + assertEquals("/", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits relative url with query only`() { + val urlDetails = UrlUtils.parse( + "/users/1?q=1&s=2&token=secret" + ) + assertEquals("/users/1", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `splits relative url with fragment only`() { + val urlDetails = UrlUtils.parse( + "/users/1#top" + ) + assertEquals("/users/1", urlDetails.url) + assertNull(urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `strips user info with user and password without query`() { + val urlDetails = UrlUtils.parse( + "https://user:password@sentry.io#top" + ) + assertEquals("https://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertNull(urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `splits without query`() { + val urlDetails = UrlUtils.parse( + "https://sentry.io#top" + ) + assertEquals("https://sentry.io", urlDetails.url) + assertNull(urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `strips user info with user and password without fragment`() { + val urlDetails = UrlUtils.parse( + "https://user:password@sentry.io?q=1&s=2&token=secret" + ) + assertEquals("https://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `strips user info with user and password without query or fragment`() { + val urlDetails = UrlUtils.parse( + "https://user:password@sentry.io" + ) + assertEquals("https://[Filtered]:[Filtered]@sentry.io", urlDetails.url) + assertNull(urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `splits url without query or fragment and no authority`() { + val urlDetails = UrlUtils.parse( + "https://sentry.io" + ) + assertEquals("https://sentry.io", urlDetails.url) + assertNull(urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `strips user info with user only`() { + val urlDetails = UrlUtils.parse( + "https://user@sentry.io?q=1&s=2&token=secret#top" + ) + assertEquals("https://[Filtered]@sentry.io", urlDetails.url) + assertEquals("q=1&s=2&token=secret", urlDetails.query) + assertEquals("top", urlDetails.fragment) + } + + @Test + fun `no details extracted with query after fragment`() { + val urlDetails = UrlUtils.parse( + "https://user:password@sentry.io#fragment?q=1&s=2&token=secret" + ) + assertNull(urlDetails.url) + assertNull(urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `no details extracted with query after fragment without authority`() { + val urlDetails = UrlUtils.parse( + "https://sentry.io#fragment?q=1&s=2&token=secret" + ) + assertNull(urlDetails.url) + assertNull(urlDetails.query) + assertNull(urlDetails.fragment) + } + + @Test + fun `no details extracted from malformed url`() { + val urlDetails = UrlUtils.parse( + "htps://user@sentry.io#fragment?q=1&s=2&token=secret" + ) + assertNull(urlDetails.url) + assertNull(urlDetails.query) + assertNull(urlDetails.fragment) + } +}