Skip to content

Commit 15bb186

Browse files
authored
Fix ANR on dropped uncaught exception events (#2368)
1 parent 507f924 commit 15bb186

File tree

3 files changed

+149
-99
lines changed

3 files changed

+149
-99
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Fix `Gpu.vendorId` should be a String ([#2343](https://github.com/getsentry/sentry-java/pull/2343))
88
- Don't set device name on Android if `sendDefaultPii` is disabled ([#2354](https://github.com/getsentry/sentry-java/pull/2354))
99
- Fix corrupted UUID on Motorola devices ([#2363](https://github.com/getsentry/sentry-java/pull/2363))
10+
- Fix ANR on dropped uncaught exception events ([#2368](https://github.com/getsentry/sentry-java/pull/2368))
1011

1112
### Features
1213

sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.sentry.hints.Flushable;
88
import io.sentry.hints.SessionEnd;
99
import io.sentry.protocol.Mechanism;
10+
import io.sentry.protocol.SentryId;
1011
import io.sentry.util.HintUtils;
1112
import io.sentry.util.Objects;
1213
import java.io.Closeable;
@@ -97,15 +98,18 @@ public void uncaughtException(Thread thread, Throwable thrown) {
9798

9899
final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint);
99100

100-
hub.captureEvent(event, hint);
101-
// Block until the event is flushed to disk
102-
if (!exceptionHint.waitFlush()) {
103-
options
104-
.getLogger()
105-
.log(
106-
SentryLevel.WARNING,
107-
"Timed out waiting to flush event to disk before crashing. Event: %s",
108-
event.getEventId());
101+
final @NotNull SentryId sentryId = hub.captureEvent(event, hint);
102+
final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
103+
if (!isEventDropped) {
104+
// Block until the event is flushed to disk
105+
if (!exceptionHint.waitFlush()) {
106+
options
107+
.getLogger()
108+
.log(
109+
SentryLevel.WARNING,
110+
"Timed out waiting to flush event to disk before crashing. Event: %s",
111+
event.getEventId());
112+
}
109113
}
110114
} catch (Throwable e) {
111115
options
Lines changed: 135 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,97 @@
11
package io.sentry
22

33
import io.sentry.exception.ExceptionMechanismException
4+
import io.sentry.hints.DiskFlushNotification
45
import io.sentry.protocol.SentryId
5-
import io.sentry.util.noFlushTimeout
6+
import io.sentry.util.HintUtils
67
import org.mockito.kotlin.any
8+
import org.mockito.kotlin.argThat
79
import org.mockito.kotlin.argWhere
810
import org.mockito.kotlin.mock
911
import org.mockito.kotlin.never
1012
import org.mockito.kotlin.verify
1113
import org.mockito.kotlin.whenever
1214
import java.io.ByteArrayOutputStream
13-
import java.io.File
1415
import java.io.PrintStream
1516
import java.nio.file.Files
16-
import kotlin.test.AfterTest
17-
import kotlin.test.BeforeTest
17+
import kotlin.concurrent.thread
1818
import kotlin.test.Test
1919
import kotlin.test.assertNotNull
2020
import kotlin.test.assertTrue
2121

2222
class UncaughtExceptionHandlerIntegrationTest {
2323

24-
private lateinit var file: File
25-
26-
@BeforeTest
27-
fun `set up`() {
28-
file = Files.createTempDirectory("sentry-disk-cache-test").toAbsolutePath().toFile()
24+
class Fixture {
25+
val file = Files.createTempDirectory("sentry-disk-cache-test").toAbsolutePath().toFile()
26+
internal val handler = mock<UncaughtExceptionHandler>()
27+
val defaultHandler = mock<Thread.UncaughtExceptionHandler>()
28+
val thread = mock<Thread>()
29+
val throwable = Throwable("test")
30+
val hub = mock<IHub>()
31+
val options = SentryOptions()
32+
val logger = mock<ILogger>()
33+
34+
fun getSut(
35+
flushTimeoutMillis: Long = 0L,
36+
hasDefaultHandler: Boolean = false,
37+
enableUncaughtExceptionHandler: Boolean = true,
38+
isPrintUncaughtStackTrace: Boolean = false
39+
): UncaughtExceptionHandlerIntegration {
40+
options.flushTimeoutMillis = flushTimeoutMillis
41+
options.isEnableUncaughtExceptionHandler = enableUncaughtExceptionHandler
42+
options.isPrintUncaughtStackTrace = isPrintUncaughtStackTrace
43+
options.setLogger(logger)
44+
options.isDebug = true
45+
whenever(handler.defaultUncaughtExceptionHandler).thenReturn(if (hasDefaultHandler) defaultHandler else null)
46+
return UncaughtExceptionHandlerIntegration(handler)
47+
}
2948
}
3049

31-
@AfterTest
32-
fun shutdown() {
33-
Files.delete(file.toPath())
34-
}
50+
private val fixture = Fixture()
3551

3652
@Test
3753
fun `when UncaughtExceptionHandlerIntegration is initialized, uncaught handler is unchanged`() {
38-
val handlerMock = mock<UncaughtExceptionHandler>()
39-
UncaughtExceptionHandlerIntegration(handlerMock)
40-
verify(handlerMock, never()).defaultUncaughtExceptionHandler = any()
54+
fixture.getSut(isPrintUncaughtStackTrace = false)
55+
56+
verify(fixture.handler, never()).defaultUncaughtExceptionHandler = any()
4157
}
4258

4359
@Test
4460
fun `when uncaughtException is called, sentry captures exception`() {
45-
val handlerMock = mock<UncaughtExceptionHandler>()
46-
val threadMock = mock<Thread>()
47-
val throwableMock = mock<Throwable>()
48-
val hubMock = mock<IHub>()
49-
val options = SentryOptions().noFlushTimeout()
50-
val sut = UncaughtExceptionHandlerIntegration(handlerMock)
51-
sut.register(hubMock, options)
52-
sut.uncaughtException(threadMock, throwableMock)
53-
verify(hubMock).captureEvent(any(), any<Hint>())
61+
val sut = fixture.getSut(isPrintUncaughtStackTrace = false)
62+
63+
sut.register(fixture.hub, fixture.options)
64+
sut.uncaughtException(fixture.thread, fixture.throwable)
65+
66+
verify(fixture.hub).captureEvent(any(), any<Hint>())
5467
}
5568

5669
@Test
5770
fun `when register is called, current handler is not lost`() {
58-
val handlerMock = mock<UncaughtExceptionHandler>()
59-
val threadMock = mock<Thread>()
60-
val throwableMock = mock<Throwable>()
61-
val defaultHandlerMock = mock<Thread.UncaughtExceptionHandler>()
62-
whenever(handlerMock.defaultUncaughtExceptionHandler).thenReturn(defaultHandlerMock)
63-
val hubMock = mock<IHub>()
64-
val options = SentryOptions().noFlushTimeout()
65-
val sut = UncaughtExceptionHandlerIntegration(handlerMock)
66-
sut.register(hubMock, options)
67-
sut.uncaughtException(threadMock, throwableMock)
68-
verify(defaultHandlerMock).uncaughtException(threadMock, throwableMock)
71+
val sut = fixture.getSut(hasDefaultHandler = true, isPrintUncaughtStackTrace = false)
72+
73+
sut.register(fixture.hub, fixture.options)
74+
sut.uncaughtException(fixture.thread, fixture.throwable)
75+
76+
verify(fixture.defaultHandler).uncaughtException(fixture.thread, fixture.throwable)
6977
}
7078

7179
@Test
7280
fun `when uncaughtException is called, exception captured has handled=false`() {
73-
val handlerMock = mock<UncaughtExceptionHandler>()
74-
val threadMock = mock<Thread>()
75-
val throwableMock = mock<Throwable>()
76-
val hubMock = mock<IHub>()
77-
whenever(hubMock.captureException(any())).thenAnswer { invocation ->
78-
val e = (invocation.arguments[1] as ExceptionMechanismException)
81+
whenever(fixture.hub.captureException(any())).thenAnswer { invocation ->
82+
val e = invocation.getArgument<ExceptionMechanismException>(1)
7983
assertNotNull(e)
8084
assertNotNull(e.exceptionMechanism)
8185
assertTrue(e.exceptionMechanism.isHandled!!)
8286
SentryId.EMPTY_ID
8387
}
84-
val options = SentryOptions().noFlushTimeout()
85-
val sut = UncaughtExceptionHandlerIntegration(handlerMock)
86-
sut.register(hubMock, options)
87-
sut.uncaughtException(threadMock, throwableMock)
88-
verify(hubMock).captureEvent(any(), any<Hint>())
88+
89+
val sut = fixture.getSut(isPrintUncaughtStackTrace = false)
90+
91+
sut.register(fixture.hub, fixture.options)
92+
sut.uncaughtException(fixture.thread, fixture.throwable)
93+
94+
verify(fixture.hub).captureEvent(any(), any<Hint>())
8995
}
9096

9197
@Test
@@ -94,63 +100,57 @@ class UncaughtExceptionHandlerIntegrationTest {
94100
val options = SentryOptions()
95101
options.dsn = "https://[email protected]/proj"
96102
options.addIntegration(integrationMock)
97-
options.cacheDirPath = file.absolutePath
103+
options.cacheDirPath = fixture.file.absolutePath
98104
options.setSerializer(mock())
99-
// val expected = HubAdapter.getInstance()
100105
val hub = Hub(options)
101-
// verify(integrationMock).register(expected, options)
102106
hub.close()
103107
verify(integrationMock).close()
104108
}
105109

106110
@Test
107111
fun `When defaultUncaughtExceptionHandler is disabled, should not install Sentry UncaughtExceptionHandler`() {
108-
val options = SentryOptions()
109-
options.isEnableUncaughtExceptionHandler = false
110-
val hub = mock<IHub>()
111-
val handlerMock = mock<UncaughtExceptionHandler>()
112-
val integration = UncaughtExceptionHandlerIntegration(handlerMock)
113-
integration.register(hub, options)
114-
verify(handlerMock, never()).defaultUncaughtExceptionHandler = any()
112+
val sut = fixture.getSut(
113+
enableUncaughtExceptionHandler = false,
114+
isPrintUncaughtStackTrace = false
115+
)
116+
117+
sut.register(fixture.hub, fixture.options)
118+
119+
verify(fixture.handler, never()).defaultUncaughtExceptionHandler = any()
115120
}
116121

117122
@Test
118123
fun `When defaultUncaughtExceptionHandler is enabled, should install Sentry UncaughtExceptionHandler`() {
119-
val options = SentryOptions()
120-
val hub = mock<IHub>()
121-
val handlerMock = mock<UncaughtExceptionHandler>()
122-
val integration = UncaughtExceptionHandlerIntegration(handlerMock)
123-
integration.register(hub, options)
124-
verify(handlerMock).defaultUncaughtExceptionHandler = argWhere { it is UncaughtExceptionHandlerIntegration }
124+
val sut = fixture.getSut(isPrintUncaughtStackTrace = false)
125+
126+
sut.register(fixture.hub, fixture.options)
127+
128+
verify(fixture.handler).defaultUncaughtExceptionHandler =
129+
argWhere { it is UncaughtExceptionHandlerIntegration }
125130
}
126131

127132
@Test
128133
fun `When defaultUncaughtExceptionHandler is set and integration is closed, default uncaught exception handler is reset to previous handler`() {
129-
val handlerMock = mock<UncaughtExceptionHandler>()
130-
val integration = UncaughtExceptionHandlerIntegration(handlerMock)
131-
132-
val defaultExceptionHandler = mock<Thread.UncaughtExceptionHandler>()
133-
whenever(handlerMock.defaultUncaughtExceptionHandler)
134-
.thenReturn(defaultExceptionHandler)
135-
integration.register(mock(), SentryOptions())
136-
whenever(handlerMock.defaultUncaughtExceptionHandler)
137-
.thenReturn(integration)
138-
integration.close()
139-
verify(handlerMock).defaultUncaughtExceptionHandler = defaultExceptionHandler
134+
val sut = fixture.getSut(hasDefaultHandler = true, isPrintUncaughtStackTrace = false)
135+
136+
sut.register(fixture.hub, fixture.options)
137+
whenever(fixture.handler.defaultUncaughtExceptionHandler)
138+
.thenReturn(sut)
139+
sut.close()
140+
141+
verify(fixture.handler).defaultUncaughtExceptionHandler = fixture.defaultHandler
140142
}
141143

142144
@Test
143145
fun `When defaultUncaughtExceptionHandler is not set and integration is closed, default uncaught exception handler is reset to null`() {
144-
val handlerMock = mock<UncaughtExceptionHandler>()
145-
val integration = UncaughtExceptionHandlerIntegration(handlerMock)
146-
147-
whenever(handlerMock.defaultUncaughtExceptionHandler)
148-
.thenReturn(null)
149-
integration.register(mock(), SentryOptions())
150-
whenever(handlerMock.defaultUncaughtExceptionHandler)
151-
.thenReturn(integration)
152-
integration.close()
153-
verify(handlerMock).defaultUncaughtExceptionHandler = null
146+
val sut = fixture.getSut(isPrintUncaughtStackTrace = false)
147+
148+
sut.register(fixture.hub, fixture.options)
149+
whenever(fixture.handler.defaultUncaughtExceptionHandler)
150+
.thenReturn(sut)
151+
sut.close()
152+
153+
verify(fixture.handler).defaultUncaughtExceptionHandler = null
154154
}
155155

156156
@Test
@@ -160,12 +160,10 @@ class UncaughtExceptionHandlerIntegrationTest {
160160
val outputStreamCaptor = ByteArrayOutputStream()
161161
System.setErr(PrintStream(outputStreamCaptor))
162162

163-
val handlerMock = mock<UncaughtExceptionHandler>()
164-
val options = SentryOptions().noFlushTimeout()
165-
options.isPrintUncaughtStackTrace = true
166-
val sut = UncaughtExceptionHandlerIntegration(handlerMock)
167-
sut.register(mock<IHub>(), options)
168-
sut.uncaughtException(mock<Thread>(), RuntimeException("This should be printed!"))
163+
val sut = fixture.getSut(isPrintUncaughtStackTrace = true)
164+
165+
sut.register(fixture.hub, fixture.options)
166+
sut.uncaughtException(fixture.thread, RuntimeException("This should be printed!"))
169167

170168
assertTrue(
171169
outputStreamCaptor.toString()
@@ -179,4 +177,51 @@ class UncaughtExceptionHandlerIntegrationTest {
179177
System.setErr(standardErr)
180178
}
181179
}
180+
181+
@Test
182+
fun `waits for event to flush on disk`() {
183+
val capturedEventId = SentryId()
184+
185+
whenever(fixture.hub.captureEvent(any(), any<Hint>())).thenAnswer { invocation ->
186+
val hint = HintUtils.getSentrySdkHint(invocation.getArgument(1))
187+
as DiskFlushNotification
188+
thread {
189+
Thread.sleep(1000L)
190+
hint.markFlushed()
191+
}
192+
capturedEventId
193+
}
194+
195+
val sut = fixture.getSut(flushTimeoutMillis = 5000)
196+
197+
sut.register(fixture.hub, fixture.options)
198+
sut.uncaughtException(fixture.thread, fixture.throwable)
199+
200+
verify(fixture.hub).captureEvent(any(), any<Hint>())
201+
// shouldn't fall into timed out state, because we marked event as flushed on another thread
202+
verify(fixture.logger, never()).log(
203+
any(),
204+
argThat { startsWith("Timed out") },
205+
any<Any>()
206+
)
207+
}
208+
209+
@Test
210+
fun `does not block flushing when the event was dropped`() {
211+
whenever(fixture.hub.captureEvent(any(), any<Hint>())).thenReturn(SentryId.EMPTY_ID)
212+
213+
val sut = fixture.getSut()
214+
215+
sut.register(fixture.hub, fixture.options)
216+
sut.uncaughtException(fixture.thread, fixture.throwable)
217+
218+
verify(fixture.hub).captureEvent(any(), any<Hint>())
219+
// we do not call markFlushed, hence it should time out waiting for flush, but because
220+
// we drop the event, it should not even come to this if-check
221+
verify(fixture.logger, never()).log(
222+
any(),
223+
argThat { startsWith("Timed out") },
224+
any<Any>()
225+
)
226+
}
182227
}

0 commit comments

Comments
 (0)