Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,21 @@ constructor(
private fun getHeader(key: String, headers: List<HttpHeader>): String? =
headers.firstOrNull { it.name.equals(key, true) }?.value

private fun getRequestHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
val requestHeaders = mutableMapOf<String, String>()
for (header in headers) {
requestHeaders[header.name] = header.value
}
return HttpUtils.filterHeaders(
requestHeaders,
scopes.options.dataCollectionResolver.httpRequestHeaders,
)
.toMutableMap()
}
return getHeaders(headers)
}

private fun getHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
// Headers are only sent if isSendDefaultPii is enabled due to PII
if (!scopes.options.isSendDefaultPii) {
Expand Down Expand Up @@ -359,7 +374,7 @@ constructor(
cookies =
if (scopes.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null
method = request.method.name
headers = getHeaders(request.headers)
headers = getRequestHeaders(request.headers)
apiTarget = "graphql"

request.body?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.apollographql.apollo3.exception.ApolloException
import io.sentry.Hint
import io.sentry.HttpBodyType
import io.sentry.IScopes
import io.sentry.KeyValueCollectionBehavior
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryOptions
import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS
Expand Down Expand Up @@ -341,6 +342,41 @@ class SentryApollo3InterceptorClientErrors {
)
}

@Test
fun `data collection filters request headers`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.httpHeaders.request = KeyValueCollectionBehavior.denyList("operation-name")
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check {
assertEquals(
"[Filtered]",
it.request!!.headers?.get("X-APOLLO-OPERATION-NAME"),
)
},
any<Hint>(),
)
}

@Test
fun `data collection can disable request headers`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.httpHeaders.request = KeyValueCollectionBehavior.off()
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check { assertTrue(it.request!!.headers!!.isEmpty()) },
any<Hint>(),
)
}

@Test
fun `capture errors with more request context if sendDefaultPii is enabled`() {
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,21 @@ constructor(
private fun getHeader(key: String, headers: List<HttpHeader>): String? =
headers.firstOrNull { it.name.equals(key, true) }?.value

private fun getRequestHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
val requestHeaders = mutableMapOf<String, String>()
for (header in headers) {
requestHeaders[header.name] = header.value
}
return HttpUtils.filterHeaders(
requestHeaders,
scopes.options.dataCollectionResolver.httpRequestHeaders,
)
.toMutableMap()
}
return getHeaders(headers)
}

private fun getHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
// Headers are only sent if isSendDefaultPii is enabled due to PII
if (!scopes.options.isSendDefaultPii) {
Expand Down Expand Up @@ -358,7 +373,7 @@ constructor(
cookies =
if (scopes.options.isSendDefaultPii) getHeader("Cookie", request.headers) else null
method = request.method.name
headers = getHeaders(request.headers)
headers = getRequestHeaders(request.headers)
apiTarget = "graphql"

request.body?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.apollographql.apollo.exception.ApolloException
import io.sentry.Hint
import io.sentry.HttpBodyType
import io.sentry.IScopes
import io.sentry.KeyValueCollectionBehavior
import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryOptions
import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS
Expand Down Expand Up @@ -355,6 +356,21 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
)
}

@Test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the data collection filters request headers test from Apollo 3 make sense here as well?

fun `data collection can disable request headers`() {
val sut =
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
dataCollection.httpHeaders.request = KeyValueCollectionBehavior.off()
}
executeQuery(sut)

verify(fixture.scopes)
.captureEvent(
check { assertTrue(it.request!!.headers!!.isEmpty()) },
any<Hint>(),
)
}

@Test
fun `capture errors with more request context if sendDefaultPii is enabled`() {
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal object SentryKtorClientUtils {
urlDetails.applyToRequest(this)
cookies = if (scopes.options.isSendDefaultPii) request.headers["Cookie"] else null
method = request.method.value
headers = getHeaders(scopes, request.headers)
headers = getRequestHeaders(scopes, request.headers)
bodySize = request.content.contentLength
}

Expand All @@ -67,6 +67,19 @@ internal object SentryKtorClientUtils {
scopes.captureEvent(event, hint)
}

private fun getRequestHeaders(scopes: IScopes, headers: Headers): MutableMap<String, String>? {
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
val requestHeaders =
headers.toMap().mapValues { (_, values) -> values.joinToString(",") }.toMutableMap()
return HttpUtils.filterHeaders(
requestHeaders,
scopes.options.dataCollectionResolver.httpRequestHeaders,
)
.toMutableMap()
}
return getHeaders(scopes, headers)
}

private fun getHeaders(scopes: IScopes, headers: Headers): MutableMap<String, String>? {
// Headers are only sent if isSendDefaultPii is enabled due to PII
if (!scopes.options.isSendDefaultPii) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.sentry.Hint
import io.sentry.HttpStatusCodeRange
import io.sentry.IScope
import io.sentry.IScopes
import io.sentry.KeyValueCollectionBehavior
import io.sentry.Scope
import io.sentry.ScopeCallback
import io.sentry.Sentry
Expand Down Expand Up @@ -255,6 +256,56 @@ class SentryKtorClientPluginTest {
verify(fixture.scopes, never()).captureEvent(any(), any<Hint>())
}

@Test
fun `data collection filters request headers`(): Unit = runBlocking {
val sut =
fixture.getSut(
captureFailedRequests = true,
httpStatusCode = 500,
optionsConfiguration =
Sentry.OptionsConfiguration {
it.dataCollection.httpHeaders.request = KeyValueCollectionBehavior.denyList("customer")
},
)

sut.get(fixture.server.url("/hello").toString()) {
headers["content-type"] = "application/json"
headers["authorization"] = "Bearer token"
headers["x-customer"] = "customer value"
}

verify(fixture.scopes)
.captureEvent(
check<SentryEvent> {
assertEquals("application/json", it.request!!.headers!!["content-type"])
assertEquals("[Filtered]", it.request!!.headers!!["authorization"])
assertEquals("[Filtered]", it.request!!.headers!!["x-customer"])
},
any<Hint>(),
)
}

@Test
fun `data collection can disable request headers`(): Unit = runBlocking {
val sut =
fixture.getSut(
captureFailedRequests = true,
httpStatusCode = 500,
optionsConfiguration =
Sentry.OptionsConfiguration {
it.dataCollection.httpHeaders.request = KeyValueCollectionBehavior.off()
},
)

sut.get(fixture.server.url("/hello").toString()) { headers["myHeader"] = "myValue" }

verify(fixture.scopes)
.captureEvent(
check<SentryEvent> { assertTrue(it.request!!.headers!!.isEmpty()) },
any<Hint>(),
)
}

@Test
fun `does not capture headers when sendDefaultPii is disabled`(): Unit = runBlocking {
val sut =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal object SentryOkHttpUtils {
// Cookie is only sent if isSendDefaultPii is enabled
cookies = if (scopes.options.isSendDefaultPii) request.headers["Cookie"] else null
method = request.method
headers = getHeaders(scopes, request.headers)
headers = getRequestHeaders(scopes, request.headers)

request.body?.contentLength().ifHasValidLength { bodySize = it }
}
Expand All @@ -67,6 +67,24 @@ internal object SentryOkHttpUtils {
}
}

private fun getRequestHeaders(
scopes: IScopes,
requestHeaders: Headers,
): MutableMap<String, String>? {
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
val headers = mutableMapOf<String, String>()
for (i in 0 until requestHeaders.size) {
headers[requestHeaders.name(i)] = requestHeaders.value(i)
}
return HttpUtils.filterHeaders(
headers,
scopes.options.dataCollectionResolver.httpRequestHeaders,
)
.toMutableMap()
}
return getHeaders(scopes, requestHeaders)
}

private fun getHeaders(scopes: IScopes, requestHeaders: Headers): MutableMap<String, String>? {
// Headers are only sent if isSendDefaultPii is enabled due to PII
if (!scopes.options.isSendDefaultPii) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.sentry.okhttp

import io.sentry.Hint
import io.sentry.IScopes
import io.sentry.KeyValueCollectionBehavior
import io.sentry.SentryOptions
import io.sentry.SentryTracer
import io.sentry.TransactionContext
Expand Down Expand Up @@ -36,12 +37,14 @@ class SentryOkHttpUtilsTest {
responseBody: String = "success",
socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN,
sendDefaultPii: Boolean = false,
configureOptions: SentryOptions.() -> Unit = {},
): OkHttpClient {
val options =
SentryOptions().apply {
dsn = "https://key@sentry.io/proj"
setTracePropagationTargets(listOf(server.hostName))
isSendDefaultPii = sendDefaultPii
configureOptions()
}
whenever(scopes.options).thenReturn(options)

Expand Down Expand Up @@ -121,6 +124,40 @@ class SentryOkHttpUtilsTest {
)
}

@Test
fun `data collection filters request headers`() {
val sut = fixture.getSut {
dataCollection.httpHeaders.request = KeyValueCollectionBehavior.denyList("myheader")
}
val request = getRequest()
val response = sut.newCall(request).execute()

SentryOkHttpUtils.captureClientError(fixture.scopes, request, response)

verify(fixture.scopes)
.captureEvent(
check {
assertEquals("[Filtered]", it.request!!.headers!!["myHeader"])
assertEquals("[Filtered]", it.request!!.headers!!["Cookie"])
},
any<Hint>(),
)
}

@Test
fun `data collection can disable request headers`() {
val sut = fixture.getSut {
dataCollection.httpHeaders.request = KeyValueCollectionBehavior.off()
}
val request = getRequest()
val response = sut.newCall(request).execute()

SentryOkHttpUtils.captureClientError(fixture.scopes, request, response)

verify(fixture.scopes)
.captureEvent(check { assertTrue(it.request!!.headers!!.isEmpty()) }, any<Hint>())
}

@Test
fun `captureClientError without sendDefaultPii does not send headers`() {
val sut = fixture.getSut(sendDefaultPii = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.SentryEvent;
import io.sentry.SentryOptions;
import io.sentry.protocol.Request;
import io.sentry.util.HttpUtils;
import io.sentry.util.Objects;
Expand All @@ -20,9 +21,12 @@
final class SentryRequestHttpServletRequestProcessor implements EventProcessor {

private final @NotNull HttpServletRequest httpRequest;
private final @NotNull SentryOptions options;

public SentryRequestHttpServletRequestProcessor(@NotNull HttpServletRequest httpRequest) {
public SentryRequestHttpServletRequestProcessor(
@NotNull HttpServletRequest httpRequest, @NotNull SentryOptions options) {
this.httpRequest = Objects.requireNonNull(httpRequest, "httpRequest is required");
this.options = Objects.requireNonNull(options, "options are required");
}

// httpRequest.getRequestURL() returns StringBuffer which is considered an obsolete class.
Expand All @@ -45,11 +49,15 @@ public SentryRequestHttpServletRequestProcessor(@NotNull HttpServletRequest http
final @NotNull HttpServletRequest request) {
final Map<String, String> headersMap = new HashMap<>();
for (String headerName : Collections.list(request.getHeaderNames())) {
// do not copy personal information identifiable headers
if (!HttpUtils.containsSensitiveHeader(headerName.toUpperCase(Locale.ROOT))) {
if (options.getDataCollectionResolver().isDataCollectionConfigured()
|| !HttpUtils.containsSensitiveHeader(headerName.toUpperCase(Locale.ROOT))) {
headersMap.put(headerName, toString(request.getHeaders(headerName)));
}
}
if (options.getDataCollectionResolver().isDataCollectionConfigured()) {
return HttpUtils.filterHeaders(
headersMap, options.getDataCollectionResolver().getHttpRequestHeaders());
}
return headersMap;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ public void requestInitialized(@NotNull ServletRequestEvent servletRequestEvent)

scopes.configureScope(
scope -> {
scope.addEventProcessor(new SentryRequestHttpServletRequestProcessor(httpRequest));
scope.addEventProcessor(
new SentryRequestHttpServletRequestProcessor(httpRequest, scopes.getOptions()));
});
}
}
Expand Down
Loading
Loading