Skip to content

Commit 1b40080

Browse files
authored
fix(anr): Use Proguard ID from origination ANR process with ANR profie chunks (#5852)
Prior to this commit, we'd always (rightly) bind originating process Proguard IDs to ANR events, but we'd (wrongly) bind current Proguard ID to ANR profile chunks. Usually the originating Proguard ID == the current Proguard ID, and so the discrepancy didn't matter. But the two differ in the situation when a user updates the host app after the ANR occurs but before it's reported. When that happens, deobfuscation for ANR profile chunks in the Sentry UI can break. This commit ensures both the ANR event and the ANR profile chunk receive the same originating Proguard ID by: 1. having the entity that determines the originating Proguard ID (ApplicationExitInfoEventProcessor) pass that ID to the profile chunk pipeline; and 2. updating our DebugMeta.buildDebugMeta(ProfileChunk, ...) method so that the profile chunk pipeline honors (rather than clobbers) the Proguard ID the event processor bakes into the DebugMeta owned by the incoming profile chunk.
1 parent 91e71ac commit 1b40080

6 files changed

Lines changed: 353 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
### Fixes
1616

17+
- Use the original app build's ProGuard UUID for ANR profile chunks ([#5852](https://github.com/getsentry/sentry-java/pull/5852))
1718
- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837))
1819
- Prevent concurrent PixelCopy access during Session Replay masking and bitmap cleanup ([#5808](https://github.com/getsentry/sentry-java/pull/5808))
1920
- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))

‎sentry-android-core/src/main/java/io/sentry/android/core/ApplicationExitInfoEventProcessor.java‎

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public ApplicationExitInfoEventProcessor(
184184
setStaticValues(event);
185185

186186
if (hintEnricher != null) {
187-
hintEnricher.applyPostEnrichment(event, backfillable, unwrappedHint);
187+
hintEnricher.applyPostEnrichment(event, backfillable, unwrappedHint, optionsSource);
188188
}
189189

190190
return event;
@@ -504,10 +504,7 @@ private void setDebugMeta(
504504
PROGUARD_UUID_FILENAME, String.class, options.getProguardUuid(), optionsSource);
505505

506506
if (proguardUuid != null) {
507-
final DebugImage debugImage = new DebugImage();
508-
debugImage.setType(DebugImage.PROGUARD);
509-
debugImage.setUuid(proguardUuid);
510-
images.add(debugImage);
507+
images.add(createProguardDebugImage(proguardUuid));
511508
}
512509
event.setDebugMeta(debugMeta);
513510
}
@@ -790,7 +787,10 @@ void applyPreEnrichment(
790787
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);
791788

792789
void applyPostEnrichment(
793-
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);
790+
@NotNull SentryEvent event,
791+
@NotNull Backfillable hint,
792+
@NotNull Object rawHint,
793+
@NotNull OptionsSource optionsSource);
794794
}
795795

796796
private final class AnrHintEnricher implements HintEnricher {
@@ -822,11 +822,14 @@ public void applyPreEnrichment(
822822

823823
@Override
824824
public void applyPostEnrichment(
825-
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
825+
@NotNull SentryEvent event,
826+
@NotNull Backfillable hint,
827+
@NotNull Object rawHint,
828+
@NotNull OptionsSource optionsSource) {
826829
final boolean isBackgroundAnr = isBackgroundAnr(rawHint);
827830

828831
if (options.isAnrProfilingEnabled()) {
829-
applyAnrProfile(event, hint, isBackgroundAnr);
832+
applyAnrProfile(event, hint, isBackgroundAnr, optionsSource);
830833
}
831834

832835
setDefaultAnrFingerprint(event, isBackgroundAnr);
@@ -920,7 +923,10 @@ private void setAnrExceptions(
920923
}
921924

922925
private void applyAnrProfile(
923-
@NotNull SentryEvent event, @NotNull Backfillable hint, boolean isBackgroundAnr) {
926+
@NotNull SentryEvent event,
927+
@NotNull Backfillable hint,
928+
boolean isBackgroundAnr,
929+
@NotNull OptionsSource optionsSource) {
924930

925931
// Skip background ANRs (as profiling only runs in foreground)
926932
if (isBackgroundAnr) {
@@ -981,7 +987,8 @@ private void applyAnrProfile(
981987
}
982988

983989
// Capture profile chunk
984-
final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
990+
final @Nullable SentryId profilerId =
991+
captureAnrProfile(anrTimestamp, anrProfile, optionsSource);
985992
final @NotNull StackTraceElement[] stack = culprit.getStack();
986993

987994
if (stack.length > 0) {
@@ -1012,7 +1019,10 @@ private void applyAnrProfile(
10121019
}
10131020

10141021
@Nullable
1015-
private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfile anrProfile) {
1022+
private SentryId captureAnrProfile(
1023+
final long anrTimestampMs,
1024+
@NotNull AnrProfile anrProfile,
1025+
final @NotNull OptionsSource optionsSource) {
10161026
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
10171027
final ProfileChunk chunk =
10181028
new ProfileChunk(
@@ -1024,6 +1034,7 @@ private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfil
10241034
ProfileChunk.PLATFORM_ANDROID,
10251035
options);
10261036
chunk.setSentryProfile(profile);
1037+
chunk.setDebugMeta(createAnrProfileDebugMeta(optionsSource));
10271038

10281039
final SentryId profilerId = Sentry.getCurrentScopes().captureProfileChunk(chunk);
10291040
if (SentryId.EMPTY_ID.equals(profilerId)) {
@@ -1058,5 +1069,37 @@ private boolean hasOnlySystemFrames(@NotNull SentryEvent event) {
10581069
}
10591070
return true;
10601071
}
1072+
1073+
/**
1074+
* Creates debug metadata for an ANR profile chunk using the build metadata selected for the ANR
1075+
* event.
1076+
*
1077+
* <p>ANR profile chunks are captured after app relaunch. If the app was updated between the ANR
1078+
* and the relaunch, the current options may contain the new build's ProGuard UUID. The provided
1079+
* {@link OptionsSource} lets us resolve the profile chunk and ANR event to the same originating
1080+
* build.
1081+
*/
1082+
private @Nullable DebugMeta createAnrProfileDebugMeta(
1083+
final @NotNull OptionsSource optionsSource) {
1084+
final String proguardUuid =
1085+
getBuildOption(
1086+
PROGUARD_UUID_FILENAME, String.class, options.getProguardUuid(), optionsSource);
1087+
if (proguardUuid == null) {
1088+
// If no historical UUID is available, let the generic profile chunk pipeline apply the
1089+
// current options UUID as its normal best-effort fallback.
1090+
return null;
1091+
}
1092+
1093+
final DebugMeta debugMeta = new DebugMeta();
1094+
debugMeta.setImages(Collections.singletonList(createProguardDebugImage(proguardUuid)));
1095+
return debugMeta;
1096+
}
1097+
}
1098+
1099+
private static @NotNull DebugImage createProguardDebugImage(final @NotNull String proguardUuid) {
1100+
final DebugImage debugImage = new DebugImage();
1101+
debugImage.setType(DebugImage.PROGUARD);
1102+
debugImage.setUuid(proguardUuid);
1103+
return debugImage;
10611104
}
10621105
}

‎sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoEventProcessorTest.kt‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.sentry.Hint
1111
import io.sentry.IScopes
1212
import io.sentry.IpAddressUtils
1313
import io.sentry.NoOpLogger
14+
import io.sentry.ProfileChunk
1415
import io.sentry.Sentry
1516
import io.sentry.SentryBaseEvent
1617
import io.sentry.SentryEvent
@@ -75,7 +76,9 @@ import org.junit.rules.TemporaryFolder
7576
import org.junit.runner.RunWith
7677
import org.mockito.Mockito.mockStatic
7778
import org.mockito.kotlin.any
79+
import org.mockito.kotlin.argumentCaptor
7880
import org.mockito.kotlin.mock
81+
import org.mockito.kotlin.verify
7982
import org.mockito.kotlin.whenever
8083
import org.robolectric.annotation.Config
8184
import org.robolectric.shadow.api.Shadow
@@ -1015,6 +1018,60 @@ class ApplicationExitInfoEventProcessorTest {
10151018
}
10161019
}
10171020

1021+
@Test
1022+
fun `uses persisted proguard uuid for ANR profile chunk after app update`() {
1023+
fixture.options.anrProfilingSampleRate = 1.0
1024+
fixture.options.proguardUuid = "current-uuid"
1025+
val processor =
1026+
fixture.getSut(
1027+
tmpDir,
1028+
populateScopeCache = false,
1029+
populateOptionsCache = false,
1030+
isSendDefaultPii = false,
1031+
)
1032+
fixture.persistOptions(PROGUARD_UUID_FILENAME, "previous-uuid")
1033+
setLastUpdateTime(2_000)
1034+
1035+
val hint =
1036+
HintUtils.createWithTypeCheckHint(
1037+
AbnormalExitHint(mechanism = "anr_foreground", timestamp = 1_000)
1038+
)
1039+
1040+
AnrProfileManager(
1041+
fixture.options,
1042+
AnrProfileRotationHelper.getFileForRecording(File(fixture.options.cacheDirPath!!)),
1043+
)
1044+
.apply {
1045+
add(
1046+
AnrStackTrace(
1047+
1_000,
1048+
arrayOf(
1049+
StackTraceElement("com.example.MyApp", "blocked", "MyApp.java", 42),
1050+
StackTraceElement("android.os.Handler", "dispatchMessage", "Handler.java", 5678),
1051+
),
1052+
)
1053+
)
1054+
close()
1055+
}
1056+
AnrProfileRotationHelper.rotate()
1057+
1058+
val scopes = mock<IScopes>()
1059+
whenever(scopes.captureProfileChunk(any())).thenReturn(SentryId())
1060+
1061+
mockStatic(Sentry::class.java).use { mockedSentry ->
1062+
mockedSentry.`when`<Any> { Sentry.getCurrentScopes() }.thenReturn(scopes)
1063+
1064+
processor.process(SentryEvent(), hint)
1065+
1066+
val chunkCaptor = argumentCaptor<ProfileChunk>()
1067+
verify(scopes).captureProfileChunk(chunkCaptor.capture())
1068+
val images = chunkCaptor.firstValue.debugMeta!!.images!!
1069+
assertEquals(1, images.size)
1070+
assertEquals(DebugImage.PROGUARD, images[0].type)
1071+
assertEquals("previous-uuid", images[0].uuid)
1072+
}
1073+
}
1074+
10181075
@Test
10191076
fun `does not crash when ANR profiling is enabled but cache dir is null`() {
10201077
fixture.options.anrProfilingSampleRate = 1.0

‎sentry/src/main/java/io/sentry/protocol/DebugMeta.java‎

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) {
5757
@ApiStatus.Internal
5858
public static @Nullable DebugMeta buildDebugMeta(
5959
final @Nullable DebugMeta eventDebugMeta, final @NotNull SentryOptions options) {
60+
final @NotNull List<DebugImage> optionDebugImages = createDebugImagesFromOptions(options);
61+
62+
if (eventDebugMeta == null && optionDebugImages.isEmpty()) {
63+
return null;
64+
}
65+
66+
final @NotNull DebugMeta debugMeta = eventDebugMeta != null ? eventDebugMeta : new DebugMeta();
67+
addMissingDebugImages(debugMeta, optionDebugImages);
68+
return debugMeta;
69+
}
70+
71+
private static @NotNull List<DebugImage> createDebugImagesFromOptions(
72+
final @NotNull SentryOptions options) {
6073
final @NotNull List<DebugImage> debugImages = new ArrayList<>();
6174

6275
if (options.getProguardUuid() != null) {
@@ -73,21 +86,56 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) {
7386
debugImages.add(sourceBundleImage);
7487
}
7588

76-
if (!debugImages.isEmpty()) {
77-
DebugMeta debugMeta = eventDebugMeta;
89+
return debugImages;
90+
}
7891

79-
if (debugMeta == null) {
80-
debugMeta = new DebugMeta();
92+
private static void addMissingDebugImages(
93+
final @NotNull DebugMeta debugMeta, final @NotNull List<DebugImage> candidates) {
94+
if (candidates.isEmpty()) {
95+
return;
96+
}
97+
98+
if (debugMeta.getImages() == null) {
99+
debugMeta.setImages(new ArrayList<>());
100+
}
101+
102+
final @Nullable List<DebugImage> images = debugMeta.getImages();
103+
if (images == null) {
104+
return;
105+
}
106+
107+
for (final @NotNull DebugImage candidate : candidates) {
108+
if (isMissingDebugImage(images, candidate)) {
109+
images.add(candidate);
81110
}
82-
if (debugMeta.getImages() == null) {
83-
debugMeta.setImages(debugImages);
84-
} else {
85-
debugMeta.getImages().addAll(debugImages);
111+
}
112+
}
113+
114+
private static boolean isMissingDebugImage(
115+
final @NotNull List<DebugImage> images, final @NotNull DebugImage candidate) {
116+
for (final @NotNull DebugImage image : images) {
117+
if (isMatchingDebugImage(image, candidate)) {
118+
return false;
86119
}
120+
}
121+
return true;
122+
}
87123

88-
return debugMeta;
124+
private static boolean isMatchingDebugImage(
125+
final @NotNull DebugImage image, final @NotNull DebugImage candidate) {
126+
// There can only be one ProGuard mapping per payload, so an existing ProGuard image takes
127+
// precedence over the option-derived default.
128+
if (DebugImage.PROGUARD.equals(candidate.getType())) {
129+
return DebugImage.PROGUARD.equals(image.getType());
89130
}
90-
return null;
131+
132+
if (DebugImage.JVM.equals(candidate.getType())) {
133+
return DebugImage.JVM.equals(image.getType())
134+
&& candidate.getDebugId() != null
135+
&& candidate.getDebugId().equals(image.getDebugId());
136+
}
137+
138+
return false;
91139
}
92140

93141
// JsonKeys

‎sentry/src/test/java/io/sentry/SentryClientTest.kt‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import io.sentry.logger.ILoggerBatchProcessorFactory
1818
import io.sentry.metrics.IMetricsBatchProcessor
1919
import io.sentry.metrics.IMetricsBatchProcessorFactory
2020
import io.sentry.protocol.Contexts
21+
import io.sentry.protocol.DebugImage
22+
import io.sentry.protocol.DebugMeta
2123
import io.sentry.protocol.Feedback
2224
import io.sentry.protocol.Mechanism
2325
import io.sentry.protocol.Message
@@ -1993,6 +1995,56 @@ class SentryClientTest {
19931995
verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId)
19941996
}
19951997

1998+
@Test
1999+
fun `captureProfileChunk adds options proguard debug meta`() {
2000+
fixture.sentryOptions.proguardUuid = "current-uuid"
2001+
2002+
val client = fixture.getSut()
2003+
client.captureProfileChunk(fixture.profileChunk, mock())
2004+
2005+
verify(fixture.transport)
2006+
.send(
2007+
check { actual ->
2008+
val profileChunk = getProfileChunkFromEnvelope(actual)
2009+
val images = profileChunk.debugMeta!!.images!!
2010+
2011+
assertEquals(1, images.size)
2012+
assertEquals(DebugImage.PROGUARD, images[0].type)
2013+
assertEquals("current-uuid", images[0].uuid)
2014+
}
2015+
)
2016+
}
2017+
2018+
@Test
2019+
fun `captureProfileChunk preserves existing proguard debug meta`() {
2020+
fixture.sentryOptions.proguardUuid = "current-uuid"
2021+
fixture.profileChunk.debugMeta =
2022+
DebugMeta().apply {
2023+
images =
2024+
listOf(
2025+
DebugImage().apply {
2026+
type = DebugImage.PROGUARD
2027+
uuid = "previous-uuid"
2028+
}
2029+
)
2030+
}
2031+
2032+
val client = fixture.getSut()
2033+
client.captureProfileChunk(fixture.profileChunk, mock())
2034+
2035+
verify(fixture.transport)
2036+
.send(
2037+
check { actual ->
2038+
val profileChunk = getProfileChunkFromEnvelope(actual)
2039+
val images = profileChunk.debugMeta!!.images!!
2040+
2041+
assertEquals(1, images.size)
2042+
assertEquals(DebugImage.PROGUARD, images[0].type)
2043+
assertEquals("previous-uuid", images[0].uuid)
2044+
}
2045+
)
2046+
}
2047+
19962048
@Test
19972049
fun `when captureProfileChunk with empty trace file, profile chunk is not sent`() {
19982050
val client = fixture.getSut()
@@ -4169,6 +4221,17 @@ class SentryClientTest {
41694221
)!!
41704222
}
41714223

4224+
private fun getProfileChunkFromData(data: ByteArray): ProfileChunk {
4225+
val inputStream = InputStreamReader(ByteArrayInputStream(data))
4226+
return fixture.sentryOptions.serializer.deserialize(inputStream, ProfileChunk::class.java)!!
4227+
}
4228+
4229+
private fun getProfileChunkFromEnvelope(envelope: SentryEnvelope): ProfileChunk {
4230+
val profileChunkItem =
4231+
envelope.items.first { item -> item.header.type == SentryItemType.ProfileChunk }
4232+
return getProfileChunkFromData(profileChunkItem.data)
4233+
}
4234+
41724235
private fun getReplayFromData(data: ByteArray): SentryReplayEvent? {
41734236
val unpacker = MessagePack.newDefaultUnpacker(data)
41744237
val mapSize = unpacker.unpackMapHeader()

0 commit comments

Comments
 (0)