Skip to content

Commit c3be389

Browse files
committed
add native attachments to TombstoneHint.
1 parent 2fd6b28 commit c3be389

File tree

2 files changed

+134
-21
lines changed

2 files changed

+134
-21
lines changed

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

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
import android.app.ApplicationExitInfo;
66
import android.content.Context;
77
import android.os.Build;
8+
import androidx.annotation.NonNull;
89
import androidx.annotation.RequiresApi;
10+
import io.sentry.Attachment;
911
import io.sentry.DateUtils;
1012
import io.sentry.Hint;
1113
import io.sentry.ILogger;
1214
import io.sentry.IScopes;
1315
import io.sentry.Integration;
16+
import io.sentry.SentryEnvelope;
17+
import io.sentry.SentryEnvelopeItem;
1418
import io.sentry.SentryEvent;
19+
import io.sentry.SentryItemType;
1520
import io.sentry.SentryLevel;
1621
import io.sentry.SentryOptions;
1722
import io.sentry.android.core.ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy;
@@ -180,37 +185,78 @@ public boolean shouldReportHistorical() {
180185
final long tombstoneTimestamp = exitInfo.getTimestamp();
181186
event.setTimestamp(DateUtils.getDateTime(tombstoneTimestamp));
182187

183-
// Try to find and remove matching native event from outbox
184-
final @Nullable NativeEventData matchingNativeEvent =
185-
nativeEventCollector.findAndRemoveMatchingNativeEvent(tombstoneTimestamp);
188+
final TombstoneHint tombstoneHint =
189+
new TombstoneHint(
190+
options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich);
191+
final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint);
186192

187-
if (matchingNativeEvent != null) {
193+
try {
194+
mergeWithMatchingNativeEvents(tombstoneTimestamp, event, hint);
195+
} catch (Throwable e) {
188196
options
189197
.getLogger()
190198
.log(
191-
SentryLevel.DEBUG,
192-
"Found matching native event for tombstone, removing from outbox: %s",
193-
matchingNativeEvent.getFile().getName());
199+
SentryLevel.WARNING,
200+
"Failed to merge native event with tombstone, continuing without merge: %s",
201+
e.getMessage());
202+
}
194203

195-
// Delete from outbox so OutboxSender doesn't send it
196-
boolean deletionSuccess = nativeEventCollector.deleteNativeEventFile(matchingNativeEvent);
204+
return new ApplicationExitInfoHistoryDispatcher.Report(event, hint, tombstoneHint);
205+
}
197206

198-
if (deletionSuccess) {
199-
event = mergeNativeCrashes(matchingNativeEvent.getEvent(), event);
200-
}
201-
} else {
207+
private void mergeWithMatchingNativeEvents(
208+
long tombstoneTimestamp, SentryEvent event, Hint hint) {
209+
// Try to find and remove matching native event from outbox
210+
final @Nullable NativeEventData matchingNativeEvent =
211+
nativeEventCollector.findAndRemoveMatchingNativeEvent(tombstoneTimestamp);
212+
213+
if (matchingNativeEvent == null) {
202214
options.getLogger().log(SentryLevel.DEBUG, "No matching native event found for tombstone.");
215+
return;
203216
}
204217

205-
final TombstoneHint tombstoneHint =
206-
new TombstoneHint(
207-
options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich);
208-
final Hint hint = HintUtils.createWithTypeCheckHint(tombstoneHint);
218+
options
219+
.getLogger()
220+
.log(
221+
SentryLevel.DEBUG,
222+
"Found matching native event for tombstone, removing from outbox: %s",
223+
matchingNativeEvent.getFile().getName());
209224

210-
return new ApplicationExitInfoHistoryDispatcher.Report(event, hint, tombstoneHint);
225+
// Delete from outbox so OutboxSender doesn't send it
226+
boolean deletionSuccess = nativeEventCollector.deleteNativeEventFile(matchingNativeEvent);
227+
228+
if (deletionSuccess) {
229+
mergeNativeCrashes(matchingNativeEvent.getEvent(), event);
230+
addNativeAttachmentsToTombstoneHint(matchingNativeEvent, hint);
231+
}
232+
}
233+
234+
private void addNativeAttachmentsToTombstoneHint(
235+
@NonNull NativeEventData matchingNativeEvent, Hint hint) {
236+
@NotNull SentryEnvelope nativeEnvelope = matchingNativeEvent.getEnvelope();
237+
for (SentryEnvelopeItem item : nativeEnvelope.getItems()) {
238+
try {
239+
@Nullable String attachmentFileName = item.getHeader().getFileName();
240+
if (item.getHeader().getType() != SentryItemType.Attachment
241+
|| attachmentFileName == null) {
242+
continue;
243+
}
244+
hint.addAttachment(
245+
new Attachment(
246+
item.getData(),
247+
attachmentFileName,
248+
item.getHeader().getContentType(),
249+
item.getHeader().getAttachmentType(),
250+
false));
251+
} catch (Throwable e) {
252+
options
253+
.getLogger()
254+
.log(SentryLevel.DEBUG, "Failed to process envelope item: %s", e.getMessage());
255+
}
256+
}
211257
}
212258

213-
private SentryEvent mergeNativeCrashes(
259+
private void mergeNativeCrashes(
214260
final @NotNull SentryEvent nativeEvent, final @NotNull SentryEvent tombstoneEvent) {
215261
// we take the event data verbatim from the Native SDK and only apply tombstone data where we
216262
// are sure that it will improve the outcome:
@@ -236,8 +282,6 @@ private SentryEvent mergeNativeCrashes(
236282
nativeEvent.setDebugMeta(tombstoneDebugMeta);
237283
nativeEvent.setThreads(tombstoneThreads);
238284
}
239-
240-
return nativeEvent;
241285
}
242286
}
243287

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@ package io.sentry.android.core
22

33
import android.app.ApplicationExitInfo
44
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import io.sentry.DateUtils
6+
import io.sentry.Hint
57
import io.sentry.SentryEvent
68
import io.sentry.SentryLevel
79
import io.sentry.android.core.TombstoneIntegration.TombstoneHint
810
import io.sentry.android.core.cache.AndroidEnvelopeCache
11+
import java.io.File
912
import java.util.zip.GZIPInputStream
13+
import kotlin.test.Test
1014
import kotlin.test.assertEquals
1115
import kotlin.test.assertNotNull
1216
import kotlin.test.assertTrue
1317
import org.junit.runner.RunWith
18+
import org.mockito.kotlin.any
19+
import org.mockito.kotlin.argThat
1420
import org.mockito.kotlin.spy
21+
import org.mockito.kotlin.verify
1522
import org.mockito.kotlin.whenever
1623
import org.robolectric.annotation.Config
1724
import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder
@@ -78,4 +85,66 @@ class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase<TombstoneHin
7885
assertEquals("0x764c32a000", image.imageAddr)
7986
assertEquals(32768, image.imageSize)
8087
}
88+
89+
@Test
90+
fun `when matching native event has attachments, they are added to the hint`() {
91+
val integration =
92+
fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) { options ->
93+
// Set up the outbox directory with the native envelope containing an attachment
94+
// Use newTimestamp to match the tombstone timestamp
95+
val outboxDir = File(options.outboxPath!!)
96+
outboxDir.mkdirs()
97+
createNativeEnvelopeWithAttachment(outboxDir, newTimestamp)
98+
}
99+
100+
// Add tombstone with timestamp matching the native event
101+
fixture.addAppExitInfo(timestamp = newTimestamp)
102+
103+
integration.register(fixture.scopes, fixture.options)
104+
105+
verify(fixture.scopes)
106+
.captureEvent(
107+
any(),
108+
argThat<Hint> {
109+
val attachments = this.attachments
110+
attachments.size == 2 &&
111+
attachments[0].filename == "test-attachment.txt" &&
112+
attachments[0].contentType == "text/plain" &&
113+
String(attachments[0].bytes!!) == "some attachment content" &&
114+
attachments[1].filename == "test-another-attachment.txt" &&
115+
attachments[1].contentType == "text/plain" &&
116+
String(attachments[1].bytes!!) == "another attachment content"
117+
},
118+
)
119+
}
120+
121+
private fun createNativeEnvelopeWithAttachment(outboxDir: File, timestamp: Long): File {
122+
val isoTimestamp = DateUtils.getTimestamp(DateUtils.getDateTime(timestamp))
123+
124+
val eventJson =
125+
"""{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc","timestamp":"$isoTimestamp","platform":"native","level":"fatal"}"""
126+
val eventJsonSize = eventJson.toByteArray(Charsets.UTF_8).size
127+
128+
val attachment1Content = "some attachment content"
129+
val attachment1ContentSize = attachment1Content.toByteArray(Charsets.UTF_8).size
130+
131+
val attachment2Content = "another attachment content"
132+
val attachment2ContentSize = attachment2Content.toByteArray(Charsets.UTF_8).size
133+
134+
val envelopeContent =
135+
"""
136+
{"event_id":"9ec79c33ec9942ab8353589fcb2e04dc"}
137+
{"type":"attachment","length":$attachment1ContentSize,"filename":"test-attachment.txt","content_type":"text/plain"}
138+
$attachment1Content
139+
{"type":"attachment","length":$attachment2ContentSize,"filename":"test-another-attachment.txt","content_type":"text/plain"}
140+
$attachment2Content
141+
{"type":"event","length":$eventJsonSize,"content_type":"application/json"}
142+
$eventJson
143+
"""
144+
.trimIndent()
145+
146+
return File(outboxDir, "native-envelope-with-attachment.envelope").apply {
147+
writeText(envelopeContent)
148+
}
149+
}
81150
}

0 commit comments

Comments
 (0)