Skip to content

Commit 14a445d

Browse files
authored
Merge pull request #5807 from getsentry/feat/data-collection-http-response-headers
feat(http): [Data Collection 11] Apply response header policy
2 parents 6347a0b + 7f7ca0e commit 14a445d

8 files changed

Lines changed: 216 additions & 4 deletions

File tree

‎sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,21 @@ constructor(
279279
return getHeaders(headers)
280280
}
281281

282+
private fun getResponseHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
283+
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
284+
val responseHeaders = mutableMapOf<String, String>()
285+
for (header in headers) {
286+
responseHeaders[header.name] = header.value
287+
}
288+
return HttpUtils.filterHeaders(
289+
responseHeaders,
290+
scopes.options.dataCollectionResolver.httpResponseHeaders,
291+
)
292+
.toMutableMap()
293+
}
294+
return getHeaders(headers)
295+
}
296+
282297
private fun getHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
283298
// Headers are only sent if isSendDefaultPii is enabled due to PII
284299
if (!scopes.options.isSendDefaultPii) {
@@ -405,7 +420,7 @@ constructor(
405420
} else {
406421
null
407422
}
408-
headers = getHeaders(response.headers)
423+
headers = getResponseHeaders(response.headers)
409424
statusCode = response.statusCode
410425

411426
response.body?.buffer?.size?.ifHasValidLength { contentLength ->

‎sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,38 @@ class SentryApollo3InterceptorClientErrors {
435435
)
436436
}
437437

438+
@Test
439+
fun `data collection filters response headers`() {
440+
val sut =
441+
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
442+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.denyList("content-length")
443+
}
444+
executeQuery(sut)
445+
446+
verify(fixture.scopes)
447+
.captureEvent(
448+
check {
449+
assertEquals("[Filtered]", it.contexts.response!!.headers?.get("Content-Length"))
450+
},
451+
any<Hint>(),
452+
)
453+
}
454+
455+
@Test
456+
fun `data collection can disable response headers`() {
457+
val sut =
458+
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
459+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.off()
460+
}
461+
executeQuery(sut)
462+
463+
verify(fixture.scopes)
464+
.captureEvent(
465+
check { assertTrue(it.contexts.response!!.headers!!.isEmpty()) },
466+
any<Hint>(),
467+
)
468+
}
469+
438470
@Test
439471
fun `capture errors with more response context if sendDefaultPii is enabled`() {
440472
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)

‎sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,21 @@ constructor(
278278
return getHeaders(headers)
279279
}
280280

281+
private fun getResponseHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
282+
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
283+
val responseHeaders = mutableMapOf<String, String>()
284+
for (header in headers) {
285+
responseHeaders[header.name] = header.value
286+
}
287+
return HttpUtils.filterHeaders(
288+
responseHeaders,
289+
scopes.options.dataCollectionResolver.httpResponseHeaders,
290+
)
291+
.toMutableMap()
292+
}
293+
return getHeaders(headers)
294+
}
295+
281296
private fun getHeaders(headers: List<HttpHeader>): MutableMap<String, String>? {
282297
// Headers are only sent if isSendDefaultPii is enabled due to PII
283298
if (!scopes.options.isSendDefaultPii) {
@@ -404,7 +419,7 @@ constructor(
404419
} else {
405420
null
406421
}
407-
headers = getHeaders(response.headers)
422+
headers = getResponseHeaders(response.headers)
408423
statusCode = response.statusCode
409424

410425
response.body?.buffer?.size?.ifHasValidLength { contentLength ->

‎sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4BuilderExtensionsClientErrorsTest.kt‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,38 @@ abstract class SentryApollo4BuilderExtensionsClientErrorsTest(
445445
)
446446
}
447447

448+
@Test
449+
fun `data collection filters response headers`() {
450+
val sut =
451+
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
452+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.denyList("content-length")
453+
}
454+
executeQuery(sut)
455+
456+
verify(fixture.scopes)
457+
.captureEvent(
458+
check {
459+
assertEquals("[Filtered]", it.contexts.response!!.headers?.get("Content-Length"))
460+
},
461+
any<Hint>(),
462+
)
463+
}
464+
465+
@Test
466+
fun `data collection can disable response headers`() {
467+
val sut =
468+
fixture.getSut(responseBody = fixture.responseBodyNotOk) {
469+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.off()
470+
}
471+
executeQuery(sut)
472+
473+
verify(fixture.scopes)
474+
.captureEvent(
475+
check { assertTrue(it.contexts.response!!.headers!!.isEmpty()) },
476+
any<Hint>(),
477+
)
478+
}
479+
448480
@Test
449481
fun `capture errors with more response context if sendDefaultPii is enabled`() {
450482
val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true)

‎sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal object SentryKtorClientUtils {
4848
io.sentry.protocol.Response().apply {
4949
// Set-Cookie is only sent if isSendDefaultPii is enabled due to PII
5050
cookies = if (scopes.options.isSendDefaultPii) response.headers["Set-Cookie"] else null
51-
headers = getHeaders(scopes, response.headers)
51+
headers = getResponseHeaders(scopes, response.headers)
5252
statusCode = response.status.value
5353
try {
5454
bodySize = response.bodyAsBytes().size.toLong()
@@ -80,6 +80,19 @@ internal object SentryKtorClientUtils {
8080
return getHeaders(scopes, headers)
8181
}
8282

83+
private fun getResponseHeaders(scopes: IScopes, headers: Headers): MutableMap<String, String>? {
84+
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
85+
val responseHeaders =
86+
headers.toMap().mapValues { (_, values) -> values.joinToString(",") }.toMutableMap()
87+
return HttpUtils.filterHeaders(
88+
responseHeaders,
89+
scopes.options.dataCollectionResolver.httpResponseHeaders,
90+
)
91+
.toMutableMap()
92+
}
93+
return getHeaders(scopes, headers)
94+
}
95+
8396
private fun getHeaders(scopes: IScopes, headers: Headers): MutableMap<String, String>? {
8497
// Headers are only sent if isSendDefaultPii is enabled due to PII
8598
if (!scopes.options.isSendDefaultPii) {

‎sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,59 @@ class SentryKtorClientPluginTest {
306306
)
307307
}
308308

309+
@Test
310+
fun `data collection filters response headers`(): Unit = runBlocking {
311+
val sut =
312+
fixture.getSut(
313+
captureFailedRequests = true,
314+
httpStatusCode = 500,
315+
optionsConfiguration =
316+
Sentry.OptionsConfiguration {
317+
it.dataCollection.httpHeaders.response = KeyValueCollectionBehavior.denyList("response")
318+
},
319+
)
320+
321+
sut.get(fixture.server.url("/hello").toString())
322+
323+
verify(fixture.scopes)
324+
.captureEvent(
325+
check<SentryEvent> {
326+
assertEquals(
327+
"[Filtered]",
328+
it.contexts.response!!
329+
.headers!!
330+
.entries
331+
.firstOrNull { header ->
332+
header.key.equals("myResponseHeader", ignoreCase = true)
333+
}
334+
?.value,
335+
)
336+
},
337+
any<Hint>(),
338+
)
339+
}
340+
341+
@Test
342+
fun `data collection can disable response headers`(): Unit = runBlocking {
343+
val sut =
344+
fixture.getSut(
345+
captureFailedRequests = true,
346+
httpStatusCode = 500,
347+
optionsConfiguration =
348+
Sentry.OptionsConfiguration {
349+
it.dataCollection.httpHeaders.response = KeyValueCollectionBehavior.off()
350+
},
351+
)
352+
353+
sut.get(fixture.server.url("/hello").toString())
354+
355+
verify(fixture.scopes)
356+
.captureEvent(
357+
check<SentryEvent> { assertTrue(it.contexts.response!!.headers!!.isEmpty()) },
358+
any<Hint>(),
359+
)
360+
}
361+
309362
@Test
310363
fun `does not capture headers when sendDefaultPii is disabled`(): Unit = runBlocking {
311364
val sut =

‎sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ internal object SentryOkHttpUtils {
4949
io.sentry.protocol.Response().apply {
5050
// Set-Cookie is only sent if isSendDefaultPii is enabled due to PII
5151
cookies = if (scopes.options.isSendDefaultPii) response.headers["Set-Cookie"] else null
52-
headers = getHeaders(scopes, response.headers)
52+
headers = getResponseHeaders(scopes, response.headers)
5353
statusCode = response.code
5454

5555
response.body?.contentLength().ifHasValidLength { bodySize = it }
@@ -85,6 +85,24 @@ internal object SentryOkHttpUtils {
8585
return getHeaders(scopes, requestHeaders)
8686
}
8787

88+
private fun getResponseHeaders(
89+
scopes: IScopes,
90+
responseHeaders: Headers,
91+
): MutableMap<String, String>? {
92+
if (scopes.options.dataCollectionResolver.isDataCollectionConfigured) {
93+
val headers = mutableMapOf<String, String>()
94+
for (i in 0 until responseHeaders.size) {
95+
headers[responseHeaders.name(i)] = responseHeaders.value(i)
96+
}
97+
return HttpUtils.filterHeaders(
98+
headers,
99+
scopes.options.dataCollectionResolver.httpResponseHeaders,
100+
)
101+
.toMutableMap()
102+
}
103+
return getHeaders(scopes, responseHeaders)
104+
}
105+
88106
private fun getHeaders(scopes: IScopes, requestHeaders: Headers): MutableMap<String, String>? {
89107
// Headers are only sent if isSendDefaultPii is enabled due to PII
90108
if (!scopes.options.isSendDefaultPii) {

‎sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,40 @@ class SentryOkHttpUtilsTest {
158158
.captureEvent(check { assertTrue(it.request!!.headers!!.isEmpty()) }, any<Hint>())
159159
}
160160

161+
@Test
162+
fun `data collection filters response headers`() {
163+
val sut = fixture.getSut {
164+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.denyList("response")
165+
}
166+
val request = getRequest()
167+
val response = sut.newCall(request).execute()
168+
169+
SentryOkHttpUtils.captureClientError(fixture.scopes, request, response)
170+
171+
verify(fixture.scopes)
172+
.captureEvent(
173+
check {
174+
assertEquals("[Filtered]", it.contexts.response!!.headers!!["myResponseHeader"])
175+
assertEquals("[Filtered]", it.contexts.response!!.headers!!["Set-Cookie"])
176+
},
177+
any<Hint>(),
178+
)
179+
}
180+
181+
@Test
182+
fun `data collection can disable response headers`() {
183+
val sut = fixture.getSut {
184+
dataCollection.httpHeaders.response = KeyValueCollectionBehavior.off()
185+
}
186+
val request = getRequest()
187+
val response = sut.newCall(request).execute()
188+
189+
SentryOkHttpUtils.captureClientError(fixture.scopes, request, response)
190+
191+
verify(fixture.scopes)
192+
.captureEvent(check { assertTrue(it.contexts.response!!.headers!!.isEmpty()) }, any<Hint>())
193+
}
194+
161195
@Test
162196
fun `captureClientError without sendDefaultPii does not send headers`() {
163197
val sut = fixture.getSut(sendDefaultPii = false)

0 commit comments

Comments
 (0)