Skip to content

Commit

Permalink
Crashed last run (#1739)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Oct 4, 2021
1 parent c3bd304 commit 3ec0698
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* Feat: Add isCrashedLastRun support (#1739)
* Fix: Handle exception if Context.registerReceiver throws (#1747)
* Feat: Attach Java vendor and version to events and transactions (#1703)

Expand Down
14 changes: 14 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public final class io/sentry/Hub : io/sentry/IHub {
public fun getLastEventId ()Lio/sentry/protocol/SentryId;
public fun getOptions ()Lio/sentry/SentryOptions;
public fun getSpan ()Lio/sentry/ISpan;
public fun isCrashedLastRun ()Ljava/lang/Boolean;
public fun isEnabled ()Z
public fun popScope ()V
public fun pushScope ()V
Expand Down Expand Up @@ -168,6 +169,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub {
public fun getLastEventId ()Lio/sentry/protocol/SentryId;
public fun getOptions ()Lio/sentry/SentryOptions;
public fun getSpan ()Lio/sentry/ISpan;
public fun isCrashedLastRun ()Ljava/lang/Boolean;
public fun isEnabled ()Z
public fun popScope ()V
public fun pushScope ()V
Expand Down Expand Up @@ -224,6 +226,7 @@ public abstract interface class io/sentry/IHub {
public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId;
public abstract fun getOptions ()Lio/sentry/SentryOptions;
public abstract fun getSpan ()Lio/sentry/ISpan;
public abstract fun isCrashedLastRun ()Ljava/lang/Boolean;
public abstract fun isEnabled ()Z
public abstract fun popScope ()V
public abstract fun pushScope ()V
Expand Down Expand Up @@ -384,6 +387,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub {
public fun getLastEventId ()Lio/sentry/protocol/SentryId;
public fun getOptions ()Lio/sentry/SentryOptions;
public fun getSpan ()Lio/sentry/ISpan;
public fun isCrashedLastRun ()Ljava/lang/Boolean;
public fun isEnabled ()Z
public fun popScope ()V
public fun pushScope ()V
Expand Down Expand Up @@ -606,6 +610,7 @@ public final class io/sentry/Sentry {
public static fun init (Lio/sentry/Sentry$OptionsConfiguration;Z)V
public static fun init (Lio/sentry/SentryOptions;)V
public static fun init (Ljava/lang/String;)V
public static fun isCrashedLastRun ()Ljava/lang/Boolean;
public static fun isEnabled ()Z
public static fun popScope ()V
public static fun pushScope ()V
Expand Down Expand Up @@ -692,6 +697,13 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient {
public fun isEnabled ()Z
}

public final class io/sentry/SentryCrashLastRunState {
public static fun getInstance ()Lio/sentry/SentryCrashLastRunState;
public fun isCrashedLastRun (Ljava/lang/String;Z)Ljava/lang/Boolean;
public fun reset ()V
public fun setCrashedLastRun (Z)V
}

public final class io/sentry/SentryEnvelope {
public fun <init> (Lio/sentry/SentryEnvelopeHeader;Ljava/lang/Iterable;)V
public fun <init> (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SdkVersion;Lio/sentry/SentryEnvelopeItem;)V
Expand Down Expand Up @@ -1327,6 +1339,8 @@ public final class io/sentry/adapters/TimeZoneSerializerAdapter : com/google/gso
}

public final class io/sentry/cache/EnvelopeCache : io/sentry/cache/IEnvelopeCache {
public static final field CRASH_MARKER_FILE Ljava/lang/String;
public static final field NATIVE_CRASH_MARKER_FILE Ljava/lang/String;
public static final field PREFIX_CURRENT_SESSION_FILE Ljava/lang/String;
public static final field SUFFIX_ENVELOPE_FILE Ljava/lang/String;
protected static final field UTF_8 Ljava/nio/charset/Charset;
Expand Down
6 changes: 6 additions & 0 deletions sentry/src/main/java/io/sentry/Hub.java
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ public void pushScope() {
return this.stack.peek().getOptions();
}

@Override
public @Nullable Boolean isCrashedLastRun() {
return SentryCrashLastRunState.getInstance()
.isCrashedLastRun(options.getCacheDirPath(), !options.isEnableAutoSessionTracking());
}

@Override
public void popScope() {
if (!isEnabled()) {
Expand Down
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/HubAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,9 @@ public void setSpanContext(
public @NotNull SentryOptions getOptions() {
return Sentry.getCurrentHub().getOptions();
}

@Override
public @Nullable Boolean isCrashedLastRun() {
return Sentry.isCrashedLastRun();
}
}
13 changes: 13 additions & 0 deletions sentry/src/main/java/io/sentry/IHub.java
Original file line number Diff line number Diff line change
Expand Up @@ -485,4 +485,17 @@ void setSpanContext(
*/
@NotNull
SentryOptions getOptions();

/**
* Returns if the App has crashed (Process has terminated) during the last run. It only returns
* true or false if offline caching {{@link SentryOptions#getCacheDirPath()} } is set with a valid
* dir.
*
* <p>If the call to this method is early in the App lifecycle and the SDK could not check if the
* App has crashed in the background, the check is gonna do IO in the calling thread.
*
* @return true if App has crashed, false otherwise, and null if not evaluated yet
*/
@Nullable
Boolean isCrashedLastRun();
}
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpHub.java
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,9 @@ public void setSpanContext(
public @NotNull SentryOptions getOptions() {
return emptyOptions;
}

@Override
public @Nullable Boolean isCrashedLastRun() {
return null;
}
}
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,20 @@ public static void endSession() {
return getCurrentHub().getSpan();
}

/**
* Returns if the App has crashed (Process has terminated) during the last run. It only returns
* true or false if offline caching {{@link SentryOptions#getCacheDirPath()} } is set with a valid
* dir.
*
* <p>If the call to this method is early in the App lifecycle and the SDK could not check if the
* App has crashed in the background, the check is gonna do IO in the calling thread.
*
* @return true if App has crashed, false otherwise, and null if not evaluated yet
*/
public static @Nullable Boolean isCrashedLastRun() {
return getCurrentHub().isCrashedLastRun();
}

/**
* Configuration options callback
*
Expand Down
81 changes: 81 additions & 0 deletions sentry/src/main/java/io/sentry/SentryCrashLastRunState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.sentry;

import io.sentry.cache.EnvelopeCache;
import java.io.File;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

@ApiStatus.Internal
public final class SentryCrashLastRunState {

private static final SentryCrashLastRunState INSTANCE = new SentryCrashLastRunState();

private boolean readCrashedLastRun;
private @Nullable Boolean crashedLastRun;

private final @NotNull Object crashedLastRunLock = new Object();

private SentryCrashLastRunState() {}

public static SentryCrashLastRunState getInstance() {
return INSTANCE;
}

public @Nullable Boolean isCrashedLastRun(
final @Nullable String cacheDirPath, final boolean deleteFile) {
synchronized (crashedLastRunLock) {
if (readCrashedLastRun) {
return crashedLastRun;
}

if (cacheDirPath == null) {
return null;
}
readCrashedLastRun = true;

final File javaMarker = new File(cacheDirPath, EnvelopeCache.CRASH_MARKER_FILE);
final File nativeMarker = new File(cacheDirPath, EnvelopeCache.NATIVE_CRASH_MARKER_FILE);
boolean exists = false;
try {
if (javaMarker.exists()) {
exists = true;

javaMarker.delete();
} else if (nativeMarker.exists()) {
exists = true;
// only delete if release health is disabled, otherwise the session crashed by the native
// side won't be marked as crashed correctly.
if (deleteFile) {
nativeMarker.delete();
}
}
} catch (Exception e) {
// ignore
}

crashedLastRun = exists;
}

return crashedLastRun;
}

public void setCrashedLastRun(final boolean crashedLastRun) {
synchronized (crashedLastRunLock) {
if (!readCrashedLastRun) {
this.crashedLastRun = crashedLastRun;
// mark readCrashedLastRun as true since its being set directly
readCrashedLastRun = true;
}
}
}

@TestOnly
public void reset() {
synchronized (crashedLastRunLock) {
readCrashedLastRun = false;
crashedLastRun = null;
}
}
}
49 changes: 47 additions & 2 deletions sentry/src/main/java/io/sentry/cache/EnvelopeCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
import static java.lang.String.format;

import io.sentry.DateUtils;
import io.sentry.SentryCrashLastRunState;
import io.sentry.SentryEnvelope;
import io.sentry.SentryEnvelopeItem;
import io.sentry.SentryItemType;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.Session;
import io.sentry.hints.DiskFlushNotification;
import io.sentry.hints.SessionEnd;
import io.sentry.hints.SessionStart;
import io.sentry.transport.NoOpEnvelopeCache;
Expand Down Expand Up @@ -51,7 +53,8 @@ public final class EnvelopeCache extends CacheStrategy implements IEnvelopeCache

public static final String PREFIX_CURRENT_SESSION_FILE = "session";
static final String SUFFIX_CURRENT_SESSION_FILE = ".json";
static final String CRASH_MARKER_FILE = ".sentry-native/last_crash";
public static final String CRASH_MARKER_FILE = "last_crash";
public static final String NATIVE_CRASH_MARKER_FILE = ".sentry-native/" + CRASH_MARKER_FILE;

private final @NotNull Map<SentryEnvelope, String> fileNameMap = new WeakHashMap<>();

Expand Down Expand Up @@ -88,6 +91,7 @@ public void store(final @NotNull SentryEnvelope envelope, final @Nullable Object
}

if (hint instanceof SessionStart) {
boolean crashedLastRun = false;

// TODO: should we move this to AppLifecycleIntegration? and do on SDK init? but it's too much
// on main-thread
Expand All @@ -107,14 +111,17 @@ public void store(final @NotNull SentryEnvelope envelope, final @Nullable Object
"Stream from path %s resulted in a null envelope.",
currentSessionFile.getAbsolutePath());
} else {
final File crashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE);
final File crashMarkerFile =
new File(options.getCacheDirPath(), NATIVE_CRASH_MARKER_FILE);
Date timestamp = null;
if (crashMarkerFile.exists()) {
options
.getLogger()
.log(INFO, "Crash marker file exists, last Session is gonna be Crashed.");

timestamp = getTimestampFromCrashMarkerFile(crashMarkerFile);

crashedLastRun = true;
if (!crashMarkerFile.delete()) {
options
.getLogger()
Expand Down Expand Up @@ -146,6 +153,28 @@ public void store(final @NotNull SentryEnvelope envelope, final @Nullable Object
}
}
updateCurrentSession(currentSessionFile, envelope);

// check java marker file if the native marker isnt there
if (!crashedLastRun) {
final File javaCrashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE);
if (javaCrashMarkerFile.exists()) {
options
.getLogger()
.log(INFO, "Crash marker file exists, crashedLastRun will return true.");

crashedLastRun = true;
if (!javaCrashMarkerFile.delete()) {
options
.getLogger()
.log(
ERROR,
"Failed to delete the crash marker file. %s.",
javaCrashMarkerFile.getAbsolutePath());
}
}
}

SentryCrashLastRunState.getInstance().setCrashedLastRun(crashedLastRun);
}

// TODO: probably we need to update the current session file for session updates to because of
Expand All @@ -167,6 +196,22 @@ public void store(final @NotNull SentryEnvelope envelope, final @Nullable Object
}

writeEnvelopeToDisk(envelopeFile, envelope);

// write file to the disk when its about to crash so crashedLastRun can be marked on restart
if (hint instanceof DiskFlushNotification) {
writeCrashMarkerFile();
}
}

private void writeCrashMarkerFile() {
final File crashMarkerFile = new File(options.getCacheDirPath(), CRASH_MARKER_FILE);
try (final OutputStream outputStream = new FileOutputStream(crashMarkerFile)) {
final String timestamp = DateUtils.getTimestamp(DateUtils.getCurrentDateTime());
outputStream.write(timestamp.getBytes(UTF_8));
outputStream.flush();
} catch (Exception e) {
options.getLogger().log(ERROR, "Error writing the crash marker file to the disk", e);
}
}

/**
Expand Down
33 changes: 29 additions & 4 deletions sentry/src/test/java/io/sentry/HubTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import io.sentry.cache.EnvelopeCache
import io.sentry.hints.SessionEndHint
import io.sentry.hints.SessionStartHint
import io.sentry.protocol.SentryId
Expand Down Expand Up @@ -1187,9 +1188,9 @@ class HubTest {

@Test
fun `when startTransaction and no tracing sampling is configured, event is not sampled`() {
val hub = generateHub(Sentry.OptionsConfiguration {
val hub = generateHub {
it.tracesSampleRate = 0.0
})
}

val transaction = hub.startTransaction("name", "op")
assertFalse(transaction.isSampled!!)
Expand Down Expand Up @@ -1221,10 +1222,10 @@ class HubTest {

@Test
fun `when tracesSampleRate and tracesSampler are not set on SentryOptions, startTransaction returns NoOp`() {
val hub = generateHub(Sentry.OptionsConfiguration {
val hub = generateHub {
it.tracesSampleRate = null
it.tracesSampler = null
})
}
val transaction = hub.startTransaction(TransactionContext("name", "op", true))
assertTrue(transaction is NoOpTransaction)
}
Expand Down Expand Up @@ -1297,6 +1298,30 @@ class HubTest {
}
// endregion

@Test
fun `isCrashedLastRun does not delete native marker if auto session is enabled`() {
val nativeMarker = File(file.absolutePath, EnvelopeCache.NATIVE_CRASH_MARKER_FILE)
nativeMarker.mkdirs()
nativeMarker.createNewFile()
val hub = generateHub() as Hub

assertTrue(hub.isCrashedLastRun!!)
assertTrue(nativeMarker.exists())
}

@Test
fun `isCrashedLastRun deletes the native marker if auto session is disabled`() {
val nativeMarker = File(file.absolutePath, EnvelopeCache.NATIVE_CRASH_MARKER_FILE)
nativeMarker.mkdirs()
nativeMarker.createNewFile()
val hub = generateHub {
it.isEnableAutoSessionTracking = false
}

assertTrue(hub.isCrashedLastRun!!)
assertFalse(nativeMarker.exists())
}

private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration<SentryOptions>? = null): IHub {
val options = SentryOptions().apply {
dsn = "https://[email protected]/proj"
Expand Down
Loading

0 comments on commit 3ec0698

Please sign in to comment.