Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,19 @@
import io.sentry.android.core.internal.util.CpuInfoUtils;
import io.sentry.cache.PersistingOptionsObserver;
import io.sentry.cache.PersistingScopeObserver;
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.protocol.App;
import io.sentry.protocol.Contexts;
import io.sentry.protocol.DebugImage;
import io.sentry.protocol.DebugMeta;
import io.sentry.protocol.Device;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.OperatingSystem;
import io.sentry.protocol.Request;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryStackTrace;
import io.sentry.protocol.SentryThread;
import io.sentry.protocol.User;
import io.sentry.util.HintUtils;
import java.util.ArrayList;
Expand Down Expand Up @@ -88,8 +92,7 @@ public AnrV2EventProcessor(
this.buildInfoProvider = buildInfoProvider;

final SentryStackTraceFactory sentryStackTraceFactory =
new SentryStackTraceFactory(
this.options.getInAppExcludes(), this.options.getInAppIncludes());
new SentryStackTraceFactory(this.options);

sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory);
}
Expand All @@ -109,7 +112,7 @@ public AnrV2EventProcessor(
// we always set exception values, platform, os and device even if the ANR is not enrich-able
// even though the OS context may change in the meantime (OS update), we consider this an
// edge-case
setExceptions(event);
setExceptions(event, unwrappedHint);
setPlatform(event);
mergeOS(event);
setDevice(event);
Expand All @@ -125,7 +128,7 @@ public AnrV2EventProcessor(

backfillScope(event);

backfillOptions(event);
backfillOptions(event, unwrappedHint);

setStaticValues(event);

Expand Down Expand Up @@ -264,22 +267,26 @@ private void setRequest(final @NotNull SentryBaseEvent event) {
// endregion

// region options persisted values
private void backfillOptions(final @NotNull SentryEvent event) {
private void backfillOptions(final @NotNull SentryEvent event, final @NotNull Object hint) {
setRelease(event);
setEnvironment(event);
setDist(event);
setDebugMeta(event);
setSdk(event);
setApp(event);
setApp(event, hint);
setOptionsTags(event);
}

private void setApp(final @NotNull SentryBaseEvent event) {
private void setApp(final @NotNull SentryBaseEvent event, final @NotNull Object hint) {
App app = event.getContexts().getApp();
if (app == null) {
app = new App();
}
app.setAppName(ContextUtils.getApplicationName(context, options.getLogger()));
// TODO: not entirely correct, because we define background ANRs as not the ones of
// IMPORTANCE_FOREGROUND, but this doesn't mean the app was in foreground when an ANR happened
// but it's our best effort for now. We could serialize AppState in theory.
app.setInForeground(!isBackgroundAnr(hint));

final PackageInfo packageInfo =
ContextUtils.getPackageInfo(context, options.getLogger(), buildInfoProvider);
Expand Down Expand Up @@ -339,10 +346,12 @@ private void setDebugMeta(final @NotNull SentryBaseEvent event) {
final String proguardUuid =
PersistingOptionsObserver.read(options, PROGUARD_UUID_FILENAME, String.class);

final DebugImage debugImage = new DebugImage();
debugImage.setType(DebugImage.PROGUARD);
debugImage.setUuid(proguardUuid);
images.add(debugImage);
if (proguardUuid != null) {
final DebugImage debugImage = new DebugImage();
debugImage.setType(DebugImage.PROGUARD);
debugImage.setUuid(proguardUuid);
images.add(debugImage);
}
event.setDebugMeta(debugMeta);
}
}
Expand Down Expand Up @@ -411,11 +420,51 @@ private void setPlatform(final @NotNull SentryBaseEvent event) {
}
}

private void setExceptions(final @NotNull SentryEvent event) {
final Throwable throwable = event.getThrowableMechanism();
if (throwable != null) {
event.setExceptions(sentryExceptionFactory.getSentryExceptions(throwable));
@Nullable
private SentryThread findMainThread(final @Nullable List<SentryThread> threads) {
if (threads != null) {
for (SentryThread thread : threads) {
final String name = thread.getName();
if (name != null && name.equals("main")) {
return thread;
}
}
}
return null;
}

// by default we assume that the ANR is foreground, unless abnormalMechanism is "anr_background"
private boolean isBackgroundAnr(final @NotNull Object hint) {
if (hint instanceof AbnormalExit) {
final String abnormalMechanism = ((AbnormalExit) hint).mechanism();
return "anr_background".equals(abnormalMechanism);
}
return false;
}

private void setExceptions(final @NotNull SentryEvent event, final @NotNull Object hint) {
// AnrV2 threads contain a thread dump from the OS, so we just search for the main thread dump
// and make an exception out of its stacktrace
final Mechanism mechanism = new Mechanism();
mechanism.setType("AppExitInfo");

final boolean isBackgroundAnr = isBackgroundAnr(hint);
String message = "ANR";
if (isBackgroundAnr) {
message = "Background " + message;
}
final ApplicationNotResponding anr =
new ApplicationNotResponding(message, Thread.currentThread());

SentryThread mainThread = findMainThread(event.getThreads());
if (mainThread == null) {
// if there's no main thread in the event threads, we just create a dummy thread so the
// exception is properly created as well, but without stacktrace
mainThread = new SentryThread();
mainThread.setStacktrace(new SentryStackTrace());
}
event.setExceptions(
sentryExceptionFactory.getSentryExceptionsFromThread(mainThread, mechanism, anr));
}

private void mergeUser(final @NotNull SentryBaseEvent event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.app.ActivityManager;
import android.app.ApplicationExitInfo;
import android.content.Context;
import android.os.Looper;
import io.sentry.DateUtils;
import io.sentry.Hint;
import io.sentry.IHub;
Expand All @@ -14,20 +13,23 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.threaddump.Lines;
import io.sentry.android.core.internal.threaddump.ThreadDumpParser;
import io.sentry.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.hints.AbnormalExit;
import io.sentry.hints.Backfillable;
import io.sentry.hints.BlockingFlushHint;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryThread;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.ICurrentDateProvider;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -221,7 +223,8 @@ private void reportAsSentryEvent(
final long anrTimestamp = exitInfo.getTimestamp();
final boolean isBackground =
exitInfo.getImportance() != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
final Throwable anrThrowable = buildAnrThrowable(exitInfo, isBackground);

final List<SentryThread> threads = parseThreadDump(exitInfo, isBackground);
final AnrV2Hint anrHint =
new AnrV2Hint(
options.getFlushTimeoutMillis(),
Expand All @@ -232,7 +235,8 @@ private void reportAsSentryEvent(

final Hint hint = HintUtils.createWithTypeCheckHint(anrHint);

final SentryEvent event = new SentryEvent(anrThrowable);
final SentryEvent event = new SentryEvent();
event.setThreads(threads);
event.setTimestamp(DateUtils.getDateTime(anrTimestamp));
event.setLevel(SentryLevel.FATAL);

Expand All @@ -251,20 +255,20 @@ private void reportAsSentryEvent(
}
}

private @NotNull Throwable buildAnrThrowable(
private @Nullable List<SentryThread> parseThreadDump(
final @NotNull ApplicationExitInfo exitInfo, final boolean isBackground) {
String message = "ANR";
if (isBackground) {
message = "Background " + message;
List<SentryThread> threads = null;
try (final BufferedReader reader =
new BufferedReader(new InputStreamReader(exitInfo.getTraceInputStream()))) {
final Lines lines = Lines.readLines(reader);

final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground);
threads = threadDumpParser.parse(lines);
} catch (Throwable e) {
options.getLogger().log(SentryLevel.WARNING, "Failed to parse ANR thread dump", e);
}

// TODO: here we should actually parse the trace file and extract the thread dump from there
// and then we could properly get the main thread stracktrace and construct a proper exception
final ApplicationNotResponding error =
new ApplicationNotResponding(message, Looper.getMainLooper().getThread());
final Mechanism mechanism = new Mechanism();
mechanism.setType("ANRv2");
return new ExceptionMechanismException(mechanism, error, error.getThread(), true);
return threads;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Adapted from https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/util/Line.java
*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.sentry.android.core.internal.threaddump;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public final class Line {
public int lineno;
public @NotNull String text;

public Line(final int lineno, final @NotNull String text) {
this.lineno = lineno;
this.text = text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Adapted from https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/util/Lines.java
*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.sentry.android.core.internal.threaddump;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* A stream of parsed lines. Can be rewound, and sub-regions cloned for recursive descent parsing.
*/
@ApiStatus.Internal
public final class Lines {
private final @NotNull ArrayList<? extends Line> mList;
private final int mMin;
private final int mMax;

/** The read position inside the list. */
public int pos;

/** Read the whole file into a Lines object. */
public static Lines readLines(final @NotNull File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return Lines.readLines(reader);
}
}

/** Read the whole file into a Lines object. */
public static Lines readLines(final @NotNull BufferedReader in) throws IOException {
final ArrayList<Line> list = new ArrayList<>();

int lineno = 0;
String text;
while ((text = in.readLine()) != null) {
lineno++;
list.add(new Line(lineno, text));
}

return new Lines(list);
}

/** Construct with a list of lines. */
public Lines(final @NotNull ArrayList<? extends Line> list) {
this.mList = list;
mMin = 0;
mMax = mList.size();
}

/** If there are more lines to read within the current range. */
public boolean hasNext() {
return pos < mMax;
}

/**
* Return the next line, or null if there are no more lines to read. Also returns null in the
* error condition where pos is before the beginning.
*/
@Nullable
public Line next() {
if (pos >= mMin && pos < mMax) {
return this.mList.get(pos++);
} else {
return null;
}
}

/** Move the read position back by one line. */
public void rewind() {
pos--;
}
}
Loading