Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- Keep dropped tombstone and ANR events dropped, instead of reporting the same app exit again at every app start ([#6002](https://github.com/getsentry/sentry-java/pull/6002))
- Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003))
- Apply `Sentry.withScope` and `Sentry.withIsolationScope` data to events captured inside the callback when `globalHubMode` is enabled ([#6004](https://github.com/getsentry/sentry-java/pull/6004))
- `globalHubMode` is enabled by default on Android, where tags, extras, contexts and level set inside the callback were silently dropped
- Scopes that are explicitly made current, e.g. via `Sentry.setCurrentScopes` or the `SentryContext` coroutine integration, are now also honoured when `globalHubMode` is enabled
Expand Down
1 change: 1 addition & 0 deletions sentry-okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
testImplementation(libs.mockito.inline)
testImplementation(libs.okhttp)
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.google.truth)
}

buildConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public open class SentryOkHttpEventListener(
private val scopes: IScopes = ScopesAdapter.getInstance(),
private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null,
) : EventListener() {
private var originalEventListener: EventListener? = null
private val originalEventListenerMap: MutableMap<Call, EventListener> = ConcurrentHashMap()

public companion object {
internal const val PROXY_SELECT_EVENT = "http.client.proxy_select_ms"
Expand Down Expand Up @@ -85,27 +85,34 @@ public open class SentryOkHttpEventListener(
) : this(scopes, originalEventListenerCreator = { originalEventListenerFactory.create(it) })

override fun callStart(call: Call) {
originalEventListener = originalEventListenerCreator?.invoke(call)
// The EventListener.Factory contract binds a listener to a single call, so the wrapped
// listener is kept per call instead of in a field shared by all concurrent calls
val originalEventListener = originalEventListenerCreator?.invoke(call)
if (originalEventListener != null) {
originalEventListenerMap[call] = originalEventListener
}
originalEventListener?.callStart(call)
// If the wrapped EventListener is ours, we can just delegate the calls,
// without creating other events that would create duplicates
if (canCreateEventSpan()) {
if (canCreateEventSpan(originalEventListener)) {
eventMap[call] = SentryOkHttpEvent(scopes, call.request())
}
}

override fun proxySelectStart(call: Call, url: HttpUrl) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.proxySelectStart(call, url)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(PROXY_SELECT_EVENT)
}

override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List<Proxy>) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.proxySelectEnd(call, url, proxies)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -117,17 +124,19 @@ public open class SentryOkHttpEventListener(
}

override fun dnsStart(call: Call, domainName: String) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.dnsStart(call, domainName)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(DNS_EVENT)
}

override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.dnsEnd(call, domainName, inetAddressList)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -140,26 +149,29 @@ public open class SentryOkHttpEventListener(
}

override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.connectStart(call, inetSocketAddress, proxy)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(CONNECT_EVENT)
}

override fun secureConnectStart(call: Call) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.secureConnectStart(call)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(SECURE_CONNECT_EVENT)
}

override fun secureConnectEnd(call: Call, handshake: Handshake?) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.secureConnectEnd(call, handshake)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -172,8 +184,9 @@ public open class SentryOkHttpEventListener(
proxy: Proxy,
protocol: Protocol?,
) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -188,8 +201,9 @@ public open class SentryOkHttpEventListener(
protocol: Protocol?,
ioe: IOException,
) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -202,53 +216,59 @@ public open class SentryOkHttpEventListener(
}

override fun connectionAcquired(call: Call, connection: Connection) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.connectionAcquired(call, connection)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(CONNECTION_EVENT)
}

override fun connectionReleased(call: Call, connection: Connection) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.connectionReleased(call, connection)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventFinish(CONNECTION_EVENT)
}

override fun requestHeadersStart(call: Call) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.requestHeadersStart(call)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(REQUEST_HEADERS_EVENT)
}

override fun requestHeadersEnd(call: Call, request: Request) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.requestHeadersEnd(call, request)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventFinish(REQUEST_HEADERS_EVENT)
}

override fun requestBodyStart(call: Call) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.requestBodyStart(call)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(REQUEST_BODY_EVENT)
}

override fun requestBodyEnd(call: Call, byteCount: Long) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.requestBodyEnd(call, byteCount)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -261,8 +281,9 @@ public open class SentryOkHttpEventListener(
}

override fun requestFailed(call: Call, ioe: IOException) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.requestFailed(call, ioe)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -282,17 +303,19 @@ public open class SentryOkHttpEventListener(
}

override fun responseHeadersStart(call: Call) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.responseHeadersStart(call)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(RESPONSE_HEADERS_EVENT)
}

override fun responseHeadersEnd(call: Call, response: Response) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.responseHeadersEnd(call, response)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -307,17 +330,19 @@ public open class SentryOkHttpEventListener(
}

override fun responseBodyStart(call: Call) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.responseBodyStart(call)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
okHttpEvent.onEventStart(RESPONSE_BODY_EVENT)
}

override fun responseBodyEnd(call: Call, byteCount: Long) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.responseBodyEnd(call, byteCount)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Expand All @@ -330,8 +355,9 @@ public open class SentryOkHttpEventListener(
}

override fun responseFailed(call: Call, ioe: IOException) {
val originalEventListener = originalEventListenerMap[call]
originalEventListener?.responseFailed(call, ioe)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return
Comment thread
markushi marked this conversation as resolved.
Expand All @@ -351,14 +377,15 @@ public open class SentryOkHttpEventListener(
}

override fun callEnd(call: Call) {
originalEventListener?.callEnd(call)
originalEventListenerMap.remove(call)?.callEnd(call)
val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return
okHttpEvent.finish()
}

override fun callFailed(call: Call, ioe: IOException) {
val originalEventListener = originalEventListenerMap.remove(call)
originalEventListener?.callFailed(call, ioe)
if (!canCreateEventSpan()) {
if (!canCreateEventSpan(originalEventListener)) {
return
}
val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return
Expand All @@ -370,26 +397,32 @@ public open class SentryOkHttpEventListener(
}

override fun canceled(call: Call) {
originalEventListener?.canceled(call)
// Unlike the other callbacks, canceled() can occur before callStart() and after
// callEnd()/callFailed(), so there is not always a listener bound to the call. Create one on
// the fly in that case, but do not put it in the map: nothing would remove it again, because
// a call that is canceled before it starts never gets a callEnd() or callFailed().
val originalEventListener =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're close! (and thanks for the great updates)

We still need to preserve the contract of EventListener.Factory that ensures only one EventListener instance is produced per Call lifecycle. Ie, the listener returned by the factory for a given call needs to be the listener that captures i) all of that call's lifecycle and ii) no other call's lifecycle.

We've fixed (ii), but we're still violating (i) in the case of cancelation because we're creating an extra listener for early and late cancel() invocations.

Possible solution

Thoughts about using a weak per-call map for the wrapped listener instead? Something like a WeakHashMap<Call, EventListener> guarded by synchronized, with a getOrCreateOriginalEventListener(call) helper used by both callStart and canceled().

That'd^^ let us avoid removing entries on callEnd / callFailed, and completed calls would be gc'd as soon as the Call instance is unreachable.

originalEventListenerMap[call] ?: originalEventListenerCreator?.invoke(call) ?: return
originalEventListener.canceled(call)
}

override fun satisfactionFailure(call: Call, response: Response) {
originalEventListener?.satisfactionFailure(call, response)
originalEventListenerMap[call]?.satisfactionFailure(call, response)
}

override fun cacheHit(call: Call, response: Response) {
originalEventListener?.cacheHit(call, response)
originalEventListenerMap[call]?.cacheHit(call, response)
}

override fun cacheMiss(call: Call) {
originalEventListener?.cacheMiss(call)
originalEventListenerMap[call]?.cacheMiss(call)
}

override fun cacheConditionalHit(call: Call, cachedResponse: Response) {
originalEventListener?.cacheConditionalHit(call, cachedResponse)
originalEventListenerMap[call]?.cacheConditionalHit(call, cachedResponse)
}

private fun canCreateEventSpan(): Boolean {
private fun canCreateEventSpan(originalEventListener: EventListener?): Boolean {
// If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener
// already did it
// In case SentryOkHttpEventListener from sentry-android-okhttp is used, the is check won't work
Expand Down
Loading
Loading