Skip to content

Commit 3403e7b

Browse files
committed
RUM-9501 prevent DatadogExceptionHandler to forward errors to Logs
1 parent 9f37c39 commit 3403e7b

File tree

2 files changed

+4
-260
lines changed

2 files changed

+4
-260
lines changed

dd-sdk-android-core/src/main/kotlin/com/datadog/android/error/internal/DatadogExceptionHandler.kt

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,6 @@ internal class DatadogExceptionHandler(
3434

3535
override fun uncaughtException(t: Thread, e: Throwable) {
3636
val threads = getThreadDumps(t, e)
37-
// write the log immediately
38-
val logsFeature = sdkCore.getFeature(Feature.LOGS_FEATURE_NAME)
39-
if (logsFeature != null) {
40-
logsFeature.sendEvent(
41-
JvmCrash.Logs(
42-
threadName = t.name,
43-
throwable = e,
44-
timestamp = System.currentTimeMillis(),
45-
message = createCrashMessage(e),
46-
loggerName = LOGGER_NAME,
47-
threads = threads
48-
)
49-
)
50-
} else {
51-
sdkCore.internalLogger.log(
52-
InternalLogger.Level.INFO,
53-
InternalLogger.Target.USER,
54-
{ MISSING_LOGS_FEATURE_INFO }
55-
)
56-
}
5737

5838
// write a RUM Error too
5939
val rumFeature = sdkCore.getFeature(Feature.RUM_FEATURE_NAME)

dd-sdk-android-core/src/test/kotlin/com/datadog/android/error/internal/DatadogExceptionHandlerTest.kt

Lines changed: 4 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import fr.xgouchet.elmyr.annotation.StringForgery
3535
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
3636
import fr.xgouchet.elmyr.junit5.ForgeExtension
3737
import org.assertj.core.api.Assertions.assertThat
38-
import org.assertj.core.data.Offset
3938
import org.junit.jupiter.api.AfterEach
4039
import org.junit.jupiter.api.BeforeEach
4140
import org.junit.jupiter.api.RepeatedTest
@@ -58,9 +57,7 @@ import org.mockito.kotlin.whenever
5857
import org.mockito.quality.Strictness
5958
import java.io.FileNotFoundException
6059
import java.io.IOException
61-
import java.util.concurrent.CountDownLatch
6260
import java.util.concurrent.ThreadPoolExecutor
63-
import java.util.concurrent.TimeUnit
6461

6562
@Extensions(
6663
ExtendWith(MockitoExtension::class),
@@ -124,266 +121,33 @@ internal class DatadogExceptionHandlerTest {
124121
Datadog.stopInstance()
125122
}
126123

127-
// region Forward to Logs
128-
129-
@Test
130-
fun `M log dev info W caught exception { no Logs feature registered }`() {
131-
// Given
132-
whenever(mockSdkCore.getFeature(Feature.LOGS_FEATURE_NAME)) doReturn null
133-
134-
Thread.setDefaultUncaughtExceptionHandler(null)
135-
testedHandler.register()
136-
val currentThread = Thread.currentThread()
137-
138-
// When
139-
testedHandler.uncaughtException(currentThread, fakeThrowable)
140-
141-
// Then
142-
mockInternalLogger.verifyLog(
143-
InternalLogger.Level.INFO,
144-
InternalLogger.Target.USER,
145-
DatadogExceptionHandler.MISSING_LOGS_FEATURE_INFO
146-
)
147-
}
148-
149124
@Test
150-
fun `M log exception W caught with no previous handler`() {
125+
fun `M do not send exception to logs W uncaughtException() {no previous handler}`() {
151126
// Given
152127
Thread.setDefaultUncaughtExceptionHandler(null)
153128
testedHandler.register()
154129
val currentThread = Thread.currentThread()
155-
val now = System.currentTimeMillis()
156130

157131
// When
158132
testedHandler.uncaughtException(currentThread, fakeThrowable)
159133

160134
// Then
161-
argumentCaptor<Any> {
162-
verify(mockLogsFeatureScope).sendEvent(capture())
163-
164-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
165-
166-
val logEvent = lastValue as JvmCrash.Logs
167-
168-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
169-
assertThat(logEvent.threadName).isEqualTo(currentThread.name)
170-
assertThat(logEvent.throwable).isSameAs(fakeThrowable)
171-
assertThat(logEvent.message).isEqualTo(fakeThrowable.message)
172-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
173-
with(logEvent.threads) {
174-
assertThat(this).isNotEmpty
175-
assertThat(filter { it.crashed }).hasSize(1)
176-
val crashedThread = first { it.crashed }
177-
assertThat(crashedThread.name).isEqualTo(currentThread.name)
178-
assertThat(crashedThread.stack).isEqualTo(fakeThrowable.loggableStackTrace())
179-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
180-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
181-
}
182-
}
183-
verifyNoInteractions(mockPreviousHandler)
135+
verifyNoInteractions(mockLogsFeatureScope)
184136
}
185137

186138
@Test
187-
fun `M log exception W caught { exception with message }`() {
139+
fun `M do not send exception to logs W uncaughtException() {previous handler}`() {
188140
// Given
189141
val currentThread = Thread.currentThread()
190-
val now = System.currentTimeMillis()
191142

192143
// When
193144
testedHandler.uncaughtException(currentThread, fakeThrowable)
194145

195146
// Then
196-
argumentCaptor<Any> {
197-
verify(mockLogsFeatureScope).sendEvent(capture())
198-
199-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
200-
201-
val logEvent = lastValue as JvmCrash.Logs
202-
203-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
204-
assertThat(logEvent.threadName).isEqualTo(currentThread.name)
205-
assertThat(logEvent.throwable).isSameAs(fakeThrowable)
206-
assertThat(logEvent.message).isEqualTo(fakeThrowable.message)
207-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
208-
with(logEvent.threads) {
209-
assertThat(this).isNotEmpty
210-
assertThat(filter { it.crashed }).hasSize(1)
211-
val crashedThread = first { it.crashed }
212-
assertThat(crashedThread.name).isEqualTo(currentThread.name)
213-
assertThat(crashedThread.stack).isEqualTo(fakeThrowable.loggableStackTrace())
214-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
215-
assertThat(nonCrashedThreadNames).isNotEmpty
216-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
217-
}
218-
}
147+
verifyNoInteractions(mockLogsFeatureScope)
219148
verify(mockPreviousHandler).uncaughtException(currentThread, fakeThrowable)
220149
}
221150

222-
@RepeatedTest(2)
223-
fun `M log exception W caught { exception without message }`(forge: Forge) {
224-
// Given
225-
val currentThread = Thread.currentThread()
226-
val now = System.currentTimeMillis()
227-
val throwable = forge.aThrowableWithoutMessage()
228-
229-
// When
230-
testedHandler.uncaughtException(currentThread, throwable)
231-
232-
// Then
233-
argumentCaptor<Any> {
234-
verify(mockLogsFeatureScope).sendEvent(capture())
235-
236-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
237-
238-
val logEvent = lastValue as JvmCrash.Logs
239-
240-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
241-
assertThat(logEvent.threadName).isEqualTo(currentThread.name)
242-
assertThat(logEvent.throwable).isSameAs(throwable)
243-
assertThat(logEvent.message)
244-
.isEqualTo("Application crash detected: ${throwable.javaClass.canonicalName}")
245-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
246-
with(logEvent.threads) {
247-
assertThat(this).isNotEmpty
248-
assertThat(filter { it.crashed }).hasSize(1)
249-
val crashedThread = first { it.crashed }
250-
assertThat(crashedThread.name).isEqualTo(currentThread.name)
251-
assertThat(crashedThread.stack).isEqualTo(throwable.loggableStackTrace())
252-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
253-
assertThat(nonCrashedThreadNames).isNotEmpty
254-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
255-
}
256-
}
257-
verify(mockPreviousHandler).uncaughtException(currentThread, throwable)
258-
}
259-
260-
@Test
261-
fun `M log exception W caught { exception without message or class }`() {
262-
// Given
263-
val currentThread = Thread.currentThread()
264-
val now = System.currentTimeMillis()
265-
val throwable = object : RuntimeException() {}
266-
267-
// When
268-
testedHandler.uncaughtException(currentThread, throwable)
269-
270-
// Then
271-
argumentCaptor<Any> {
272-
verify(mockLogsFeatureScope).sendEvent(capture())
273-
274-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
275-
276-
val logEvent = lastValue as JvmCrash.Logs
277-
278-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
279-
assertThat(logEvent.threadName).isEqualTo(currentThread.name)
280-
assertThat(logEvent.throwable).isSameAs(throwable)
281-
assertThat(logEvent.message)
282-
.isEqualTo("Application crash detected: ${throwable.javaClass.simpleName}")
283-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
284-
with(logEvent.threads) {
285-
assertThat(this).isNotEmpty
286-
assertThat(filter { it.crashed }).hasSize(1)
287-
val crashedThread = first { it.crashed }
288-
assertThat(crashedThread.name).isEqualTo(currentThread.name)
289-
assertThat(crashedThread.stack).isEqualTo(throwable.loggableStackTrace())
290-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
291-
assertThat(nonCrashedThreadNames).isNotEmpty
292-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
293-
}
294-
}
295-
verify(mockPreviousHandler).uncaughtException(currentThread, throwable)
296-
}
297-
298-
@Test
299-
fun `M log exception W caught on background thread`(forge: Forge) {
300-
// Given
301-
val latch = CountDownLatch(1)
302-
val threadName = forge.anAlphabeticalString()
303-
304-
// When
305-
val thread = Thread(
306-
{
307-
testedHandler.uncaughtException(Thread.currentThread(), fakeThrowable)
308-
latch.countDown()
309-
},
310-
threadName
311-
)
312-
313-
val now = System.currentTimeMillis()
314-
thread.start()
315-
latch.await(1, TimeUnit.SECONDS)
316-
317-
// Then
318-
argumentCaptor<Any> {
319-
verify(mockLogsFeatureScope).sendEvent(capture())
320-
321-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
322-
323-
val logEvent = lastValue as JvmCrash.Logs
324-
325-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
326-
assertThat(logEvent.threadName).isEqualTo(thread.name)
327-
assertThat(logEvent.throwable).isSameAs(fakeThrowable)
328-
assertThat(logEvent.message).isEqualTo(fakeThrowable.message)
329-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
330-
with(logEvent.threads) {
331-
assertThat(this).isNotEmpty
332-
assertThat(filter { it.crashed }).hasSize(1)
333-
val crashedThread = first { it.crashed }
334-
assertThat(crashedThread.name).isEqualTo(thread.name)
335-
assertThat(crashedThread.stack).isEqualTo(fakeThrowable.loggableStackTrace())
336-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
337-
assertThat(nonCrashedThreadNames).isNotEmpty
338-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
339-
}
340-
}
341-
verify(mockPreviousHandler).uncaughtException(thread, fakeThrowable)
342-
}
343-
344-
@Test
345-
fun `M log exception with a crashed thread provided W caught`() {
346-
// Given
347-
val externalLock = CountDownLatch(1)
348-
val internalLock = CountDownLatch(1)
349-
val crashedThread = Thread {
350-
externalLock.countDown()
351-
internalLock.await()
352-
}.apply { start() }
353-
// need to wait here because thread is not necessarily running yet after `start()` call
354-
externalLock.await()
355-
val now = System.currentTimeMillis()
356-
357-
// When
358-
testedHandler.uncaughtException(crashedThread, fakeThrowable)
359-
internalLock.countDown()
360-
361-
// Then
362-
argumentCaptor<Any> {
363-
verify(mockLogsFeatureScope).sendEvent(capture())
364-
365-
assertThat(lastValue).isInstanceOf(JvmCrash.Logs::class.java)
366-
367-
val logEvent = lastValue as JvmCrash.Logs
368-
369-
assertThat(logEvent.timestamp).isCloseTo(now, Offset.offset(250))
370-
assertThat(logEvent.threadName).isEqualTo(crashedThread.name)
371-
assertThat(logEvent.throwable).isSameAs(fakeThrowable)
372-
assertThat(logEvent.message).isEqualTo(fakeThrowable.message)
373-
assertThat(logEvent.loggerName).isEqualTo(DatadogExceptionHandler.LOGGER_NAME)
374-
with(logEvent.threads) {
375-
assertThat(this).isNotEmpty
376-
assertThat(filter { it.crashed }).hasSize(1)
377-
assertThat(first { it.crashed }.name).isEqualTo(crashedThread.name)
378-
assertThat(first { it.crashed }.stack).isEqualTo(fakeThrowable.loggableStackTrace())
379-
val nonCrashedThreadNames = filterNot { it.crashed }.map { it.name }
380-
assertThat(nonCrashedThreadNames).isNotEmpty
381-
assertThat(nonCrashedThreadNames).doesNotContain(crashedThread.name)
382-
}
383-
}
384-
verify(mockPreviousHandler).uncaughtException(crashedThread, fakeThrowable)
385-
}
386-
387151
// endregion
388152

389153
@Test

0 commit comments

Comments
 (0)