Skip to content
Merged
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 @@ -8,6 +8,7 @@

### Fixes

- Use the original app build's ProGuard UUID for ANR profile chunks ([#5852](https://github.com/getsentry/sentry-java/pull/5852))
- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837))
- Prevent concurrent PixelCopy access during Session Replay masking and bitmap cleanup ([#5808](https://github.com/getsentry/sentry-java/pull/5808))
- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public ApplicationExitInfoEventProcessor(
setStaticValues(event);

if (hintEnricher != null) {
hintEnricher.applyPostEnrichment(event, backfillable, unwrappedHint);
hintEnricher.applyPostEnrichment(event, backfillable, unwrappedHint, optionsSource);
}

return event;
Expand Down Expand Up @@ -504,10 +504,7 @@ private void setDebugMeta(
PROGUARD_UUID_FILENAME, String.class, options.getProguardUuid(), optionsSource);

if (proguardUuid != null) {
final DebugImage debugImage = new DebugImage();
debugImage.setType(DebugImage.PROGUARD);
debugImage.setUuid(proguardUuid);
images.add(debugImage);
images.add(createProguardDebugImage(proguardUuid));
}
event.setDebugMeta(debugMeta);
}
Expand Down Expand Up @@ -790,7 +787,10 @@ void applyPreEnrichment(
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);

void applyPostEnrichment(
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint);
@NotNull SentryEvent event,
@NotNull Backfillable hint,
@NotNull Object rawHint,
@NotNull OptionsSource optionsSource);
}

private final class AnrHintEnricher implements HintEnricher {
Expand Down Expand Up @@ -822,11 +822,14 @@ public void applyPreEnrichment(

@Override
public void applyPostEnrichment(
@NotNull SentryEvent event, @NotNull Backfillable hint, @NotNull Object rawHint) {
@NotNull SentryEvent event,
@NotNull Backfillable hint,
@NotNull Object rawHint,
@NotNull OptionsSource optionsSource) {
final boolean isBackgroundAnr = isBackgroundAnr(rawHint);

if (options.isAnrProfilingEnabled()) {
applyAnrProfile(event, hint, isBackgroundAnr);
applyAnrProfile(event, hint, isBackgroundAnr, optionsSource);
}

setDefaultAnrFingerprint(event, isBackgroundAnr);
Expand Down Expand Up @@ -920,7 +923,10 @@ private void setAnrExceptions(
}

private void applyAnrProfile(
@NotNull SentryEvent event, @NotNull Backfillable hint, boolean isBackgroundAnr) {
@NotNull SentryEvent event,
@NotNull Backfillable hint,
boolean isBackgroundAnr,
@NotNull OptionsSource optionsSource) {

// Skip background ANRs (as profiling only runs in foreground)
if (isBackgroundAnr) {
Expand Down Expand Up @@ -981,7 +987,8 @@ private void applyAnrProfile(
}

// Capture profile chunk
final @Nullable SentryId profilerId = captureAnrProfile(anrTimestamp, anrProfile);
final @Nullable SentryId profilerId =
captureAnrProfile(anrTimestamp, anrProfile, optionsSource);
final @NotNull StackTraceElement[] stack = culprit.getStack();

if (stack.length > 0) {
Expand Down Expand Up @@ -1012,7 +1019,10 @@ private void applyAnrProfile(
}

@Nullable
private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfile anrProfile) {
private SentryId captureAnrProfile(
final long anrTimestampMs,
@NotNull AnrProfile anrProfile,
final @NotNull OptionsSource optionsSource) {
final SentryProfile profile = StackTraceConverter.convert(anrProfile);
final ProfileChunk chunk =
new ProfileChunk(
Expand All @@ -1024,6 +1034,7 @@ private SentryId captureAnrProfile(final long anrTimestampMs, @NotNull AnrProfil
ProfileChunk.PLATFORM_JAVA,
options);
chunk.setSentryProfile(profile);
chunk.setDebugMeta(createAnrProfileDebugMeta(optionsSource));

final SentryId profilerId = Sentry.getCurrentScopes().captureProfileChunk(chunk);
if (SentryId.EMPTY_ID.equals(profilerId)) {
Expand Down Expand Up @@ -1058,5 +1069,37 @@ private boolean hasOnlySystemFrames(@NotNull SentryEvent event) {
}
return true;
}

/**
* Creates debug metadata for an ANR profile chunk using the build metadata selected for the ANR
* event.
*
* <p>ANR profile chunks are captured after app relaunch. If the app was updated between the ANR
* and the relaunch, the current options may contain the new build's ProGuard UUID. The provided
* {@link OptionsSource} lets us resolve the profile chunk and ANR event to the same originating
* build.
*/
private @Nullable DebugMeta createAnrProfileDebugMeta(
final @NotNull OptionsSource optionsSource) {
final String proguardUuid =
getBuildOption(
PROGUARD_UUID_FILENAME, String.class, options.getProguardUuid(), optionsSource);
if (proguardUuid == null) {
// If no historical UUID is available, let the generic profile chunk pipeline apply the
// current options UUID as its normal best-effort fallback.
return null;
}

final DebugMeta debugMeta = new DebugMeta();
debugMeta.setImages(Collections.singletonList(createProguardDebugImage(proguardUuid)));
return debugMeta;
}
}

private static @NotNull DebugImage createProguardDebugImage(final @NotNull String proguardUuid) {
final DebugImage debugImage = new DebugImage();
debugImage.setType(DebugImage.PROGUARD);
debugImage.setUuid(proguardUuid);
return debugImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.sentry.Hint
import io.sentry.IScopes
import io.sentry.IpAddressUtils
import io.sentry.NoOpLogger
import io.sentry.ProfileChunk
import io.sentry.Sentry
import io.sentry.SentryBaseEvent
import io.sentry.SentryEvent
Expand Down Expand Up @@ -75,7 +76,9 @@ import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.mockito.Mockito.mockStatic
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
Expand Down Expand Up @@ -1015,6 +1018,60 @@ class ApplicationExitInfoEventProcessorTest {
}
}

@Test
fun `uses persisted proguard uuid for ANR profile chunk after app update`() {
fixture.options.anrProfilingSampleRate = 1.0
fixture.options.proguardUuid = "current-uuid"
val processor =
fixture.getSut(
tmpDir,
populateScopeCache = false,
populateOptionsCache = false,
isSendDefaultPii = false,
)
fixture.persistOptions(PROGUARD_UUID_FILENAME, "previous-uuid")
setLastUpdateTime(2_000)

val hint =
HintUtils.createWithTypeCheckHint(
AbnormalExitHint(mechanism = "anr_foreground", timestamp = 1_000)
)

AnrProfileManager(
Comment thread
0xadam-brown marked this conversation as resolved.
fixture.options,
AnrProfileRotationHelper.getFileForRecording(File(fixture.options.cacheDirPath!!)),
)
.apply {
add(
AnrStackTrace(
1_000,
arrayOf(
StackTraceElement("com.example.MyApp", "blocked", "MyApp.java", 42),
StackTraceElement("android.os.Handler", "dispatchMessage", "Handler.java", 5678),
),
)
)
close()
}
AnrProfileRotationHelper.rotate()

val scopes = mock<IScopes>()
whenever(scopes.captureProfileChunk(any())).thenReturn(SentryId())

mockStatic(Sentry::class.java).use { mockedSentry ->
mockedSentry.`when`<Any> { Sentry.getCurrentScopes() }.thenReturn(scopes)

processor.process(SentryEvent(), hint)

val chunkCaptor = argumentCaptor<ProfileChunk>()
verify(scopes).captureProfileChunk(chunkCaptor.capture())
val images = chunkCaptor.firstValue.debugMeta!!.images!!
assertEquals(1, images.size)
assertEquals(DebugImage.PROGUARD, images[0].type)
assertEquals("previous-uuid", images[0].uuid)
}
}

@Test
fun `does not crash when ANR profiling is enabled but cache dir is null`() {
fixture.options.anrProfilingSampleRate = 1.0
Expand Down
68 changes: 58 additions & 10 deletions sentry/src/main/java/io/sentry/protocol/DebugMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) {
@ApiStatus.Internal
public static @Nullable DebugMeta buildDebugMeta(
final @Nullable DebugMeta eventDebugMeta, final @NotNull SentryOptions options) {
final @NotNull List<DebugImage> optionDebugImages = createDebugImagesFromOptions(options);

if (eventDebugMeta == null && optionDebugImages.isEmpty()) {
return null;
}

final @NotNull DebugMeta debugMeta = eventDebugMeta != null ? eventDebugMeta : new DebugMeta();
addMissingDebugImages(debugMeta, optionDebugImages);
return debugMeta;
}

private static @NotNull List<DebugImage> createDebugImagesFromOptions(
final @NotNull SentryOptions options) {
final @NotNull List<DebugImage> debugImages = new ArrayList<>();

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

if (!debugImages.isEmpty()) {
DebugMeta debugMeta = eventDebugMeta;
return debugImages;
}

if (debugMeta == null) {
debugMeta = new DebugMeta();
private static void addMissingDebugImages(
final @NotNull DebugMeta debugMeta, final @NotNull List<DebugImage> candidates) {
if (candidates.isEmpty()) {
return;
}

if (debugMeta.getImages() == null) {
debugMeta.setImages(new ArrayList<>());
}

final @Nullable List<DebugImage> images = debugMeta.getImages();
if (images == null) {
return;
}

for (final @NotNull DebugImage candidate : candidates) {
if (isMissingDebugImage(images, candidate)) {
images.add(candidate);
}
if (debugMeta.getImages() == null) {
debugMeta.setImages(debugImages);
} else {
debugMeta.getImages().addAll(debugImages);
}
}

private static boolean isMissingDebugImage(
final @NotNull List<DebugImage> images, final @NotNull DebugImage candidate) {
for (final @NotNull DebugImage image : images) {
if (isMatchingDebugImage(image, candidate)) {
return false;
}
}
return true;
}

return debugMeta;
private static boolean isMatchingDebugImage(
final @NotNull DebugImage image, final @NotNull DebugImage candidate) {
// There can only be one ProGuard mapping per payload, so an existing ProGuard image takes
Comment thread
0xadam-brown marked this conversation as resolved.
// precedence over the option-derived default.
if (DebugImage.PROGUARD.equals(candidate.getType())) {
return DebugImage.PROGUARD.equals(image.getType());
}
return null;

if (DebugImage.JVM.equals(candidate.getType())) {
return DebugImage.JVM.equals(image.getType())
&& candidate.getDebugId() != null
&& candidate.getDebugId().equals(image.getDebugId());
}

return false;
}

// JsonKeys
Expand Down
63 changes: 63 additions & 0 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import io.sentry.logger.ILoggerBatchProcessorFactory
import io.sentry.metrics.IMetricsBatchProcessor
import io.sentry.metrics.IMetricsBatchProcessorFactory
import io.sentry.protocol.Contexts
import io.sentry.protocol.DebugImage
import io.sentry.protocol.DebugMeta
import io.sentry.protocol.Feedback
import io.sentry.protocol.Mechanism
import io.sentry.protocol.Message
Expand Down Expand Up @@ -1993,6 +1995,56 @@ class SentryClientTest {
verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId)
}

@Test
fun `captureProfileChunk adds options proguard debug meta`() {
fixture.sentryOptions.proguardUuid = "current-uuid"

val client = fixture.getSut()
client.captureProfileChunk(fixture.profileChunk, mock())

verify(fixture.transport)
.send(
check { actual ->
val profileChunk = getProfileChunkFromEnvelope(actual)
val images = profileChunk.debugMeta!!.images!!

assertEquals(1, images.size)
assertEquals(DebugImage.PROGUARD, images[0].type)
assertEquals("current-uuid", images[0].uuid)
}
)
}

@Test
fun `captureProfileChunk preserves existing proguard debug meta`() {
fixture.sentryOptions.proguardUuid = "current-uuid"
fixture.profileChunk.debugMeta =
DebugMeta().apply {
images =
listOf(
DebugImage().apply {
type = DebugImage.PROGUARD
uuid = "previous-uuid"
}
)
}

val client = fixture.getSut()
client.captureProfileChunk(fixture.profileChunk, mock())

verify(fixture.transport)
.send(
check { actual ->
val profileChunk = getProfileChunkFromEnvelope(actual)
val images = profileChunk.debugMeta!!.images!!

assertEquals(1, images.size)
assertEquals(DebugImage.PROGUARD, images[0].type)
assertEquals("previous-uuid", images[0].uuid)
}
)
}

@Test
fun `when captureProfileChunk with empty trace file, profile chunk is not sent`() {
val client = fixture.getSut()
Expand Down Expand Up @@ -4169,6 +4221,17 @@ class SentryClientTest {
)!!
}

private fun getProfileChunkFromData(data: ByteArray): ProfileChunk {
val inputStream = InputStreamReader(ByteArrayInputStream(data))
return fixture.sentryOptions.serializer.deserialize(inputStream, ProfileChunk::class.java)!!
}

private fun getProfileChunkFromEnvelope(envelope: SentryEnvelope): ProfileChunk {
val profileChunkItem =
envelope.items.first { item -> item.header.type == SentryItemType.ProfileChunk }
return getProfileChunkFromData(profileChunkItem.data)
}

private fun getReplayFromData(data: ByteArray): SentryReplayEvent? {
val unpacker = MessagePack.newDefaultUnpacker(data)
val mapSize = unpacker.unpackMapHeader()
Expand Down
Loading
Loading