-
-
Notifications
You must be signed in to change notification settings - Fork 475
Capture event for HTTP requests resulted in server error #2287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4adc744
371db9a
090a7df
20f8b09
76c4567
a664017
d046806
54cac05
3804a5c
d970ff9
2c113a9
bee4510
1541330
cf81c16
412bc0e
9136b37
2284f88
6268a19
566d70e
9b5e6bb
176e7ef
e896271
67fc1c1
0e27884
a6211b8
2f10f79
a26cde4
d452d9d
d27d2c2
e3bfc1f
c06174f
76168e0
3fd6b01
7572c8a
68408b5
99b62a3
f0a6294
a3ca145
38aeba2
6a4013b
57d3776
636ed7e
b79a165
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,23 +3,50 @@ package io.sentry.android.okhttp | |
| import io.sentry.BaggageHeader | ||
| import io.sentry.Breadcrumb | ||
| import io.sentry.Hint | ||
| import io.sentry.HttpStatusCodeRange | ||
| import io.sentry.HubAdapter | ||
| import io.sentry.IHub | ||
| import io.sentry.ISpan | ||
| import io.sentry.SentryEvent | ||
| import io.sentry.SpanStatus | ||
| import io.sentry.TracePropagationTargets | ||
| import io.sentry.TypeCheckHint.OKHTTP_REQUEST | ||
| import io.sentry.TypeCheckHint.OKHTTP_RESPONSE | ||
| import io.sentry.exception.ExceptionMechanismException | ||
| import io.sentry.exception.SentryHttpClientException | ||
| import io.sentry.protocol.Mechanism | ||
| import io.sentry.util.HttpUtils | ||
| import io.sentry.util.PropagationTargetsUtils | ||
| import okhttp3.Headers | ||
| import okhttp3.Interceptor | ||
| import okhttp3.Request | ||
| import okhttp3.Response | ||
| import java.io.IOException | ||
|
|
||
| /** | ||
| * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span | ||
| * out of the active span bound to the scope for each HTTP Request. | ||
| * If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well. | ||
| * | ||
| * @param hub The [IHub], internal and only used for testing. | ||
| * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. | ||
| * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, | ||
| * Defaults to false. | ||
| * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response | ||
| * status code is within the defined ranges. | ||
| * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL | ||
| * is a match for any of the defined targets. | ||
| */ | ||
| class SentryOkHttpInterceptor( | ||
| private val hub: IHub = HubAdapter.getInstance(), | ||
| private val beforeSpan: BeforeSpanCallback? = null | ||
| private val beforeSpan: BeforeSpanCallback? = null, | ||
| private val captureFailedRequests: Boolean = false, | ||
| private val failedRequestStatusCodes: List<HttpStatusCodeRange> = listOf( | ||
| HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) | ||
| ), | ||
| private val failedRequestTargets: List<String> = listOf(".*") | ||
|
marandaneto marked this conversation as resolved.
|
||
| ) : Interceptor { | ||
|
|
||
| constructor() : this(HubAdapter.getInstance()) | ||
| constructor(hub: IHub) : this(hub, null) | ||
| constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) | ||
|
|
||
|
|
@@ -38,7 +65,7 @@ class SentryOkHttpInterceptor( | |
| try { | ||
| val requestBuilder = request.newBuilder() | ||
| if (span != null && | ||
| TracePropagationTargets.contain(hub.options.tracePropagationTargets, request.url.toString()) | ||
| PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url.toString()) | ||
| ) { | ||
| span.toSentryTrace().let { | ||
| requestBuilder.addHeader(it.name, it.value) | ||
|
|
@@ -53,6 +80,12 @@ class SentryOkHttpInterceptor( | |
| response = chain.proceed(request) | ||
| code = response.code | ||
| span?.status = SpanStatus.fromHttpStatusCode(code) | ||
|
|
||
| // OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block. | ||
| // breadcrumbs are added on the finally block because we'd like to know if the device | ||
| // had an unstable connection or something similar | ||
| captureEvent(request, response) | ||
|
|
||
| return response | ||
| } catch (e: IOException) { | ||
| span?.apply { | ||
|
|
@@ -104,6 +137,110 @@ class SentryOkHttpInterceptor( | |
| } | ||
| } | ||
|
|
||
| private fun captureEvent(request: Request, response: Response) { | ||
| // return if the feature is disabled or its not within the range | ||
| if (!captureFailedRequests || !containsStatusCode(response.code)) { | ||
| return | ||
| } | ||
|
|
||
| // not possible to get a parameterized url, but we remove at least the | ||
| // query string and the fragment. | ||
| // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query | ||
| // 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", "") | ||
| } | ||
|
|
||
| // return if its not a target match | ||
| if (!PropagationTargetsUtils.contain(failedRequestTargets, requestUrl)) { | ||
| return | ||
| } | ||
|
|
||
| val mechanism = Mechanism().apply { | ||
| type = "SentryOkHttpInterceptor" | ||
| } | ||
| val exception = SentryHttpClientException( | ||
| "HTTP Client Error with status code: ${response.code}" | ||
| ) | ||
| val mechanismException = ExceptionMechanismException(mechanism, exception, Thread.currentThread(), true) | ||
| val event = SentryEvent(mechanismException) | ||
|
|
||
| val hint = Hint() | ||
| hint.set(OKHTTP_REQUEST, request) | ||
| hint.set(OKHTTP_RESPONSE, response) | ||
|
|
||
| val sentryRequest = io.sentry.protocol.Request().apply { | ||
| url = requestUrl | ||
| // Cookie is only sent if isSendDefaultPii is enabled | ||
| cookies = if (hub.options.isSendDefaultPii) request.headers["Cookie"] else null | ||
|
marandaneto marked this conversation as resolved.
|
||
| method = request.method | ||
| queryString = query | ||
| headers = getHeaders(request.headers) | ||
| fragment = urlFragment | ||
|
|
||
| request.body?.contentLength().ifHasValidLength { | ||
| bodySize = it | ||
| } | ||
| } | ||
|
|
||
| val sentryResponse = io.sentry.protocol.Response().apply { | ||
| // Cookie is only sent if isSendDefaultPii is enabled due to PII | ||
| cookies = if (hub.options.isSendDefaultPii) response.headers["Cookie"] else null | ||
| headers = getHeaders(response.headers) | ||
| statusCode = response.code | ||
|
|
||
| response.body?.contentLength().ifHasValidLength { | ||
| bodySize = it | ||
| } | ||
| } | ||
|
|
||
| event.request = sentryRequest | ||
| event.contexts.setResponse(sentryResponse) | ||
|
|
||
| hub.captureEvent(event, hint) | ||
| } | ||
|
|
||
| private fun containsStatusCode(statusCode: Int): Boolean { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this could be called something like Also should it be extracted into some util so it can be reused from other integrations?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, |
||
| for (item in failedRequestStatusCodes) { | ||
| if (item.isInRange(statusCode)) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun getHeaders(requestHeaders: Headers): MutableMap<String, String>? { | ||
| // Headers are only sent if isSendDefaultPii is enabled due to PII | ||
| if (!hub.options.isSendDefaultPii) { | ||
| return null | ||
| } | ||
|
|
||
| val headers = mutableMapOf<String, String>() | ||
|
|
||
| for (i in 0 until requestHeaders.size) { | ||
| val name = requestHeaders.name(i) | ||
|
|
||
| // header is only sent if isn't sensitive | ||
| if (HttpUtils.containsSensitiveHeader(name)) { | ||
| continue | ||
| } | ||
|
|
||
| val value = requestHeaders.value(i) | ||
| headers[name] = value | ||
| } | ||
| return headers | ||
| } | ||
|
|
||
| /** | ||
| * The BeforeSpan callback | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.