Skip to content
Open
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 @@ -5,6 +5,7 @@
### Fixes

- Android: Add proguard rules to prevent error about missing Replay classes ([#5153](https://github.com/getsentry/sentry-java/pull/5153))
- Android: Remove the dependency on protobuf-lite for tombstones ([#5157](https://github.com/getsentry/sentry-java/pull/5157))

## 8.34.0

Expand Down
5 changes: 1 addition & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ spotless = "7.0.4"
gummyBears = "0.12.0"
camerax = "1.3.0"
openfeature = "1.18.2"
protobuf = "3.25.8"

[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Expand All @@ -61,7 +60,6 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
protobuf = { id = "com.google.protobuf", version = "0.9.5" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
Expand Down Expand Up @@ -145,8 +143,7 @@ otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:openteleme
otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" }
otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" }
p6spy = { module = "p6spy:p6spy", version = "3.9.1" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf"}
protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
epitaph = { module = "com.abovevacant:epitaph", version = "0.1.0" }
quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
Expand Down
10 changes: 1 addition & 9 deletions sentry-android-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ plugins {
alias(libs.plugins.jacoco.android)
alias(libs.plugins.errorprone)
alias(libs.plugins.gradle.versions)
alias(libs.plugins.protobuf)
}

android {
Expand Down Expand Up @@ -84,7 +83,7 @@ dependencies {
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.core)
implementation(libs.protobuf.javalite)
implementation(libs.epitaph)

errorprone(libs.errorprone.core)
errorprone(libs.nopen.checker)
Expand Down Expand Up @@ -113,10 +112,3 @@ dependencies {
testRuntimeOnly(libs.androidx.fragment.ktx)
testRuntimeOnly(libs.timber)
}

protobuf {
protoc { artifact = libs.protoc.get().toString() }
generateProtoTasks {
all().forEach { task -> task.builtins { create("java") { option("lite") } } }
}
}
3 changes: 0 additions & 3 deletions sentry-android-core/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@

-keepnames class io.sentry.android.core.ApplicationNotResponding

# protobuf-java lite
# https://github.com/protocolbuffers/protobuf/blob/5d876c9fec1a6f2feb0750694f803f89312bffff/java/lite.md#r8-rule-to-make-production-app-builds-work
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

##---------------End: proguard configuration for android-core ----------

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package io.sentry.android.core.internal.tombstone;

import androidx.annotation.NonNull;
import com.abovevacant.epitaph.core.BacktraceFrame;
import com.abovevacant.epitaph.core.MemoryMapping;
import com.abovevacant.epitaph.core.Register;
import com.abovevacant.epitaph.core.Signal;
import com.abovevacant.epitaph.core.Tombstone;
import com.abovevacant.epitaph.core.TombstoneThread;
import com.abovevacant.epitaph.wire.TombstoneDecoder;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryStackTraceFactory;
Expand All @@ -27,7 +34,7 @@

public class TombstoneParser implements Closeable {

private final InputStream tombstoneStream;
@Nullable private final InputStream tombstoneStream;
@NotNull private final List<String> inAppIncludes;
@NotNull private final List<String> inAppExcludes;
@Nullable private final String nativeLibraryDir;
Expand All @@ -38,7 +45,14 @@ private static String formatHex(long value) {
}

public TombstoneParser(
@NonNull final InputStream tombstoneStream,
@NotNull List<String> inAppIncludes,
@NotNull List<String> inAppExcludes,
@Nullable String nativeLibraryDir) {
this(null, inAppIncludes, inAppExcludes, nativeLibraryDir);
}

public TombstoneParser(
@Nullable final InputStream tombstoneStream,
@NotNull List<String> inAppIncludes,
@NotNull List<String> inAppExcludes,
@Nullable String nativeLibraryDir) {
Expand All @@ -58,10 +72,14 @@ public TombstoneParser(

@NonNull
public SentryEvent parse() throws IOException {
@NonNull
final TombstoneProtos.Tombstone tombstone =
TombstoneProtos.Tombstone.parseFrom(tombstoneStream);
if (tombstoneStream == null) {
throw new IOException("No InputStream provided; use parse(Tombstone) instead.");
}
return parse(TombstoneDecoder.decode(tombstoneStream));
}

@NonNull
public SentryEvent parse(@NonNull final Tombstone tombstone) {
final SentryEvent event = new SentryEvent();
event.setLevel(SentryLevel.FATAL);

Expand All @@ -79,19 +97,18 @@ public SentryEvent parse() throws IOException {

@NonNull
private List<SentryThread> createThreads(
@NonNull final TombstoneProtos.Tombstone tombstone, @NonNull final SentryException exc) {
@NonNull final Tombstone tombstone, @NonNull final SentryException exc) {
final List<SentryThread> threads = new ArrayList<>();
for (Map.Entry<Integer, TombstoneProtos.Thread> threadEntry :
tombstone.getThreadsMap().entrySet()) {
final TombstoneProtos.Thread threadEntryValue = threadEntry.getValue();
for (Map.Entry<Integer, TombstoneThread> threadEntry : tombstone.threads.entrySet()) {
final TombstoneThread threadEntryValue = threadEntry.getValue();

final SentryThread thread = new SentryThread();
thread.setId(Long.valueOf(threadEntry.getKey()));
thread.setName(threadEntryValue.getName());
thread.setName(threadEntryValue.name);

final SentryStackTrace stacktrace = createStackTrace(threadEntryValue);
thread.setStacktrace(stacktrace);
if (tombstone.getTid() == threadEntryValue.getId()) {
if (tombstone.tid == threadEntryValue.id) {
thread.setCrashed(true);
// even though we refer to the thread_id from the exception,
// the backend currently requires a stack-trace in exception
Expand All @@ -104,38 +121,38 @@ private List<SentryThread> createThreads(
}

@NonNull
private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread thread) {
private SentryStackTrace createStackTrace(@NonNull final TombstoneThread thread) {
final List<SentryStackFrame> frames = new ArrayList<>();

for (TombstoneProtos.BacktraceFrame frame : thread.getCurrentBacktraceList()) {
if (frame.getFileName().endsWith("libart.so")) {
for (BacktraceFrame frame : thread.backtrace) {
if (frame.fileName.endsWith("libart.so")) {
// We ignore all ART frames for time being because they aren't actionable for app developers
continue;
}
if (frame.getFileName().startsWith("<anonymous") && frame.getFunctionName().isEmpty()) {
if (frame.fileName.startsWith("<anonymous") && frame.functionName.isEmpty()) {
// Code in anonymous VMAs that does not resolve to a function name, cannot be symbolicated
// in the backend either, and thus has no value in the UI.
continue;
}
final SentryStackFrame stackFrame = new SentryStackFrame();
stackFrame.setPackage(frame.getFileName());
stackFrame.setFunction(frame.getFunctionName());
stackFrame.setInstructionAddr(formatHex(frame.getPc()));
stackFrame.setPackage(frame.fileName);
stackFrame.setFunction(frame.functionName);
stackFrame.setInstructionAddr(formatHex(frame.pc));

// inAppIncludes/inAppExcludes filter by Java/Kotlin package names, which don't overlap
// with native C/C++ function names (e.g., "crash", "__libc_init"). For native frames,
// isInApp() returns null, making nativeLibraryDir the effective in-app check.
// Protobuf returns "" for unset function names, which would incorrectly return true
// epitaph returns "" for unset function names, which would incorrectly return true
// from isInApp(), so we treat empty as false to let nativeLibraryDir decide.
final String functionName = frame.getFunctionName();
final String functionName = frame.functionName;
@Nullable
Boolean inApp =
functionName.isEmpty()
? Boolean.FALSE
: SentryStackTraceFactory.isInApp(functionName, inAppIncludes, inAppExcludes);

final boolean isInNativeLibraryDir =
nativeLibraryDir != null && frame.getFileName().startsWith(nativeLibraryDir);
nativeLibraryDir != null && frame.fileName.startsWith(nativeLibraryDir);
inApp = (inApp != null && inApp) || isInNativeLibraryDir;

stackFrame.setInApp(inApp);
Expand All @@ -151,74 +168,73 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
stacktrace.setInstructionAddressAdjustment(SentryStackTrace.InstructionAddressAdjustment.NONE);

final Map<String, String> registers = new HashMap<>();
for (TombstoneProtos.Register register : thread.getRegistersList()) {
registers.put(register.getName(), formatHex(register.getU64()));
for (Register register : thread.registers) {
registers.put(register.name, formatHex(register.value));
}
stacktrace.setRegisters(registers);

return stacktrace;
}

@NonNull
private List<SentryException> createException(@NonNull TombstoneProtos.Tombstone tombstone) {
private List<SentryException> createException(@NonNull Tombstone tombstone) {
final SentryException exception = new SentryException();

if (tombstone.hasSignalInfo()) {
final TombstoneProtos.Signal signalInfo = tombstone.getSignalInfo();
exception.setType(signalInfo.getName());
exception.setValue(excTypeValueMap.get(signalInfo.getName()));
if (tombstone.hasSignal()) {
final Signal signalInfo = tombstone.signal;
exception.setType(signalInfo.name);
exception.setValue(excTypeValueMap.get(signalInfo.name));
exception.setMechanism(createMechanismFromSignalInfo(signalInfo));
}

exception.setThreadId((long) tombstone.getTid());
exception.setThreadId((long) tombstone.tid);
final List<SentryException> exceptions = new ArrayList<>(1);
exceptions.add(exception);

return exceptions;
}

@NonNull
private static Mechanism createMechanismFromSignalInfo(
@NonNull final TombstoneProtos.Signal signalInfo) {
private static Mechanism createMechanismFromSignalInfo(@NonNull final Signal signalInfo) {

final Mechanism mechanism = new Mechanism();
mechanism.setType(NativeExceptionMechanism.TOMBSTONE.getValue());
mechanism.setHandled(false);
mechanism.setSynthetic(true);

final Map<String, Object> meta = new HashMap<>();
meta.put("number", signalInfo.getNumber());
meta.put("name", signalInfo.getName());
meta.put("code", signalInfo.getCode());
meta.put("code_name", signalInfo.getCodeName());
meta.put("number", signalInfo.number);
meta.put("name", signalInfo.name);
meta.put("code", signalInfo.code);
meta.put("code_name", signalInfo.codeName);
mechanism.setMeta(meta);

return mechanism;
}

@NonNull
private Message constructMessage(@NonNull final TombstoneProtos.Tombstone tombstone) {
private Message constructMessage(@NonNull final Tombstone tombstone) {
final Message message = new Message();
final TombstoneProtos.Signal signalInfo = tombstone.getSignalInfo();
final Signal signalInfo = tombstone.signal;

// reproduce the message `debuggerd` would use to dump the stack trace in logcat
String command = String.join(" ", tombstone.getCommandLineList());
if (tombstone.hasSignalInfo()) {
String abortMessage = tombstone.getAbortMessage();
String command = String.join(" ", tombstone.commandLine);
if (tombstone.hasSignal()) {
String abortMessage = tombstone.abortMessage;
message.setFormatted(
String.format(
Locale.ROOT,
"%sFatal signal %s (%d), %s (%d), pid = %d (%s)",
!abortMessage.isEmpty() ? abortMessage + ": " : "",
signalInfo.getName(),
signalInfo.getNumber(),
signalInfo.getCodeName(),
signalInfo.getCode(),
tombstone.getPid(),
signalInfo.name,
signalInfo.number,
signalInfo.codeName,
signalInfo.code,
tombstone.pid,
command));
} else {
message.setFormatted(
String.format(Locale.ROOT, "Fatal exit pid = %d (%s)", tombstone.getPid(), command));
String.format(Locale.ROOT, "Fatal exit pid = %d (%s)", tombstone.pid, command));
}

return message;
Expand All @@ -236,11 +252,11 @@ private static class ModuleAccumulator {
long beginAddress;
long endAddress;

ModuleAccumulator(TombstoneProtos.MemoryMapping mapping) {
this.mappingName = mapping.getMappingName();
this.buildId = mapping.getBuildId();
this.beginAddress = mapping.getBeginAddress();
this.endAddress = mapping.getEndAddress();
ModuleAccumulator(MemoryMapping mapping) {
this.mappingName = mapping.mappingName;
this.buildId = mapping.buildId;
this.beginAddress = mapping.beginAddress;
this.endAddress = mapping.endAddress;
}

void extendTo(long newEndAddress) {
Expand All @@ -266,7 +282,7 @@ DebugImage toDebugImage() {
}
}

private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombstone) {
private DebugMeta createDebugMeta(@NonNull final Tombstone tombstone) {
final List<DebugImage> images = new ArrayList<>();

// Coalesce memory mappings into modules similar to how sentry-native does it.
Expand All @@ -277,27 +293,27 @@ private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombs
// combined with non-empty build_id as a proxy for this check.
ModuleAccumulator currentModule = null;

for (TombstoneProtos.MemoryMapping mapping : tombstone.getMemoryMappingsList()) {
for (MemoryMapping mapping : tombstone.memoryMappings) {
// Skip mappings that are not readable
if (!mapping.getRead()) {
if (!mapping.read) {
continue;
}

// Skip mappings with empty name or in /dev/
final String mappingName = mapping.getMappingName();
final String mappingName = mapping.mappingName;
if (mappingName.isEmpty() || mappingName.startsWith("/dev/")) {
continue;
}

final boolean hasBuildId = !mapping.getBuildId().isEmpty();
final boolean isFileStart = mapping.getOffset() == 0;
final boolean hasBuildId = !mapping.buildId.isEmpty();
final boolean isFileStart = mapping.offset == 0;

if (hasBuildId && isFileStart) {
// Check for duplicated mappings: On Android, the same ELF can have multiple
// mappings at offset 0 with different permissions (r--p, r-xp, r--p).
// If it's the same file as the current module, just extend it.
if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
currentModule.extendTo(mapping.getEndAddress());
currentModule.extendTo(mapping.endAddress);
continue;
}

Expand All @@ -313,7 +329,7 @@ private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombs
currentModule = new ModuleAccumulator(mapping);
} else if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
// Extend the current module with this mapping (same file, continuation)
currentModule.extendTo(mapping.getEndAddress());
currentModule.extendTo(mapping.endAddress);
}
}

Expand All @@ -333,6 +349,8 @@ private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombs

@Override
public void close() throws IOException {
tombstoneStream.close();
if (tombstoneStream != null) {
tombstoneStream.close();
}
}
}
Loading
Loading