X Tutup
Skip to content

Commit ba49577

Browse files
Capture event for HTTP requests resulted in server error (#2287)
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent 418a49e commit ba49577

File tree

45 files changed

+1107
-246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1107
-246
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Profile envelopes are sent directly from profiler ([#2298](https://github.com/getsentry/sentry-java/pull/2298))
1616
- Add support for using Encoder with logback.SentryAppender ([#2246](https://github.com/getsentry/sentry-java/pull/2246))
1717
- Report Startup Crashes ([#2277](https://github.com/getsentry/sentry-java/pull/2277))
18+
- HTTP Client errors for OkHttp ([#2287](https://github.com/getsentry/sentry-java/pull/2287))
1819

1920
### Dependencies
2021

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ final class ManifestMetadataReader {
7878
static final String CLIENT_REPORTS_ENABLE = "io.sentry.send-client-reports";
7979
static final String COLLECT_ADDITIONAL_CONTEXT = "io.sentry.additional-context";
8080

81+
static final String SEND_DEFAULT_PII = "io.sentry.send-default-pii";
82+
8183
/** ManifestMetadataReader ctor */
8284
private ManifestMetadataReader() {}
8385

@@ -297,6 +299,9 @@ static void applyMetadata(
297299
sdkInfo.setName(readStringNotNull(metadata, logger, SDK_NAME, sdkInfo.getName()));
298300
sdkInfo.setVersion(readStringNotNull(metadata, logger, SDK_VERSION, sdkInfo.getVersion()));
299301
options.setSdkVersion(sdkInfo);
302+
303+
options.setSendDefaultPii(
304+
readBool(metadata, logger, SEND_DEFAULT_PII, options.isSendDefaultPii()));
300305
}
301306

302307
options

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,4 +1061,29 @@ class ManifestMetadataReaderTest {
10611061
// Assert
10621062
assertTrue(fixture.options.isCollectAdditionalContext)
10631063
}
1064+
1065+
@Test
1066+
fun `applyMetadata reads send default pii and keep default value if not found`() {
1067+
// Arrange
1068+
val context = fixture.getContext()
1069+
1070+
// Act
1071+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1072+
1073+
// Assert
1074+
assertFalse(fixture.options.isSendDefaultPii)
1075+
}
1076+
1077+
@Test
1078+
fun `applyMetadata reads send default pii to options`() {
1079+
// Arrange
1080+
val bundle = bundleOf(ManifestMetadataReader.SEND_DEFAULT_PII to true)
1081+
val context = fixture.getContext(metaData = bundle)
1082+
1083+
// Act
1084+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
1085+
1086+
// Assert
1087+
assertTrue(fixture.options.isSendDefaultPii)
1088+
}
10641089
}

sentry-android-okhttp/api/sentry-android-okhttp.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public final class io/sentry/android/okhttp/BuildConfig {
99
public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor {
1010
public fun <init> ()V
1111
public fun <init> (Lio/sentry/IHub;)V
12-
public fun <init> (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V
13-
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
12+
public fun <init> (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V
13+
public synthetic fun <init> (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
1414
public fun <init> (Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V
1515
public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response;
1616
}

sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,50 @@ package io.sentry.android.okhttp
33
import io.sentry.BaggageHeader
44
import io.sentry.Breadcrumb
55
import io.sentry.Hint
6+
import io.sentry.HttpStatusCodeRange
67
import io.sentry.HubAdapter
78
import io.sentry.IHub
89
import io.sentry.ISpan
10+
import io.sentry.SentryEvent
911
import io.sentry.SpanStatus
10-
import io.sentry.TracePropagationTargets
1112
import io.sentry.TypeCheckHint.OKHTTP_REQUEST
1213
import io.sentry.TypeCheckHint.OKHTTP_RESPONSE
14+
import io.sentry.exception.ExceptionMechanismException
15+
import io.sentry.exception.SentryHttpClientException
16+
import io.sentry.protocol.Mechanism
17+
import io.sentry.util.HttpUtils
18+
import io.sentry.util.PropagationTargetsUtils
19+
import okhttp3.Headers
1320
import okhttp3.Interceptor
1421
import okhttp3.Request
1522
import okhttp3.Response
1623
import java.io.IOException
1724

25+
/**
26+
* The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span
27+
* out of the active span bound to the scope for each HTTP Request.
28+
* If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well.
29+
*
30+
* @param hub The [IHub], internal and only used for testing.
31+
* @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback].
32+
* @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled,
33+
* Defaults to false.
34+
* @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response
35+
* status code is within the defined ranges.
36+
* @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL
37+
* is a match for any of the defined targets.
38+
*/
1839
class SentryOkHttpInterceptor(
1940
private val hub: IHub = HubAdapter.getInstance(),
20-
private val beforeSpan: BeforeSpanCallback? = null
41+
private val beforeSpan: BeforeSpanCallback? = null,
42+
private val captureFailedRequests: Boolean = false,
43+
private val failedRequestStatusCodes: List<HttpStatusCodeRange> = listOf(
44+
HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX)
45+
),
46+
private val failedRequestTargets: List<String> = listOf(".*")
2147
) : Interceptor {
2248

49+
constructor() : this(HubAdapter.getInstance())
2350
constructor(hub: IHub) : this(hub, null)
2451
constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan)
2552

@@ -38,7 +65,7 @@ class SentryOkHttpInterceptor(
3865
try {
3966
val requestBuilder = request.newBuilder()
4067
if (span != null &&
41-
TracePropagationTargets.contain(hub.options.tracePropagationTargets, request.url.toString())
68+
PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url.toString())
4269
) {
4370
span.toSentryTrace().let {
4471
requestBuilder.addHeader(it.name, it.value)
@@ -53,6 +80,12 @@ class SentryOkHttpInterceptor(
5380
response = chain.proceed(request)
5481
code = response.code
5582
span?.status = SpanStatus.fromHttpStatusCode(code)
83+
84+
// OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block.
85+
// breadcrumbs are added on the finally block because we'd like to know if the device
86+
// had an unstable connection or something similar
87+
captureEvent(request, response)
88+
5689
return response
5790
} catch (e: IOException) {
5891
span?.apply {
@@ -104,6 +137,110 @@ class SentryOkHttpInterceptor(
104137
}
105138
}
106139

140+
private fun captureEvent(request: Request, response: Response) {
141+
// return if the feature is disabled or its not within the range
142+
if (!captureFailedRequests || !containsStatusCode(response.code)) {
143+
return
144+
}
145+
146+
// not possible to get a parameterized url, but we remove at least the
147+
// query string and the fragment.
148+
// url example: https://api.github.com/users/getsentry/repos/#fragment?query=query
149+
// url will be: https://api.github.com/users/getsentry/repos/
150+
// ideally we'd like a parameterized url: https://api.github.com/users/{user}/repos/
151+
// but that's not possible
152+
var requestUrl = request.url.toString()
153+
154+
val query = request.url.query
155+
if (!query.isNullOrEmpty()) {
156+
requestUrl = requestUrl.replace("?$query", "")
157+
}
158+
159+
val urlFragment = request.url.fragment
160+
if (!urlFragment.isNullOrEmpty()) {
161+
requestUrl = requestUrl.replace("#$urlFragment", "")
162+
}
163+
164+
// return if its not a target match
165+
if (!PropagationTargetsUtils.contain(failedRequestTargets, requestUrl)) {
166+
return
167+
}
168+
169+
val mechanism = Mechanism().apply {
170+
type = "SentryOkHttpInterceptor"
171+
}
172+
val exception = SentryHttpClientException(
173+
"HTTP Client Error with status code: ${response.code}"
174+
)
175+
val mechanismException = ExceptionMechanismException(mechanism, exception, Thread.currentThread(), true)
176+
val event = SentryEvent(mechanismException)
177+
178+
val hint = Hint()
179+
hint.set(OKHTTP_REQUEST, request)
180+
hint.set(OKHTTP_RESPONSE, response)
181+
182+
val sentryRequest = io.sentry.protocol.Request().apply {
183+
url = requestUrl
184+
// Cookie is only sent if isSendDefaultPii is enabled
185+
cookies = if (hub.options.isSendDefaultPii) request.headers["Cookie"] else null
186+
method = request.method
187+
queryString = query
188+
headers = getHeaders(request.headers)
189+
fragment = urlFragment
190+
191+
request.body?.contentLength().ifHasValidLength {
192+
bodySize = it
193+
}
194+
}
195+
196+
val sentryResponse = io.sentry.protocol.Response().apply {
197+
// Cookie is only sent if isSendDefaultPii is enabled due to PII
198+
cookies = if (hub.options.isSendDefaultPii) response.headers["Cookie"] else null
199+
headers = getHeaders(response.headers)
200+
statusCode = response.code
201+
202+
response.body?.contentLength().ifHasValidLength {
203+
bodySize = it
204+
}
205+
}
206+
207+
event.request = sentryRequest
208+
event.contexts.setResponse(sentryResponse)
209+
210+
hub.captureEvent(event, hint)
211+
}
212+
213+
private fun containsStatusCode(statusCode: Int): Boolean {
214+
for (item in failedRequestStatusCodes) {
215+
if (item.isInRange(statusCode)) {
216+
return true
217+
}
218+
}
219+
return false
220+
}
221+
222+
private fun getHeaders(requestHeaders: Headers): MutableMap<String, String>? {
223+
// Headers are only sent if isSendDefaultPii is enabled due to PII
224+
if (!hub.options.isSendDefaultPii) {
225+
return null
226+
}
227+
228+
val headers = mutableMapOf<String, String>()
229+
230+
for (i in 0 until requestHeaders.size) {
231+
val name = requestHeaders.name(i)
232+
233+
// header is only sent if isn't sensitive
234+
if (HttpUtils.containsSensitiveHeader(name)) {
235+
continue
236+
}
237+
238+
val value = requestHeaders.value(i)
239+
headers[name] = value
240+
}
241+
return headers
242+
}
243+
107244
/**
108245
* The BeforeSpan callback
109246
*/

0 commit comments

Comments
 (0)
X Tutup