Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report Startup Crashes #2277

Merged
merged 17 commits into from Oct 17, 2022
Merged
Expand Up @@ -24,6 +24,7 @@ public void onCreate() {
// });
// */
// });
//throw new RuntimeException("Startup Crash!");
romtsn marked this conversation as resolved.
Show resolved Hide resolved
}

private void strictMode() {
Expand Down
@@ -1,7 +1,11 @@
package io.sentry;

import io.sentry.cache.EnvelopeCache;
import io.sentry.util.Objects;
import java.io.File;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -23,6 +27,9 @@ public interface SendFireAndForgetFactory {
@Nullable
SendFireAndForget create(@NotNull IHub hub, @NotNull SentryOptions options);

@Nullable
String getDirPath();

default boolean hasValidPath(final @Nullable String dirPath, final @NotNull ILogger logger) {
if (dirPath == null) {
logger.log(SentryLevel.INFO, "No cached dir path is defined in options.");
Expand Down Expand Up @@ -71,7 +78,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions
}

try {
options
Future<?> future = options
.getExecutorService()
.submit(
() -> {
Expand All @@ -84,6 +91,25 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions
}
});

final String dirPath = factory.getDirPath();
romtsn marked this conversation as resolved.
Show resolved Hide resolved
if (dirPath != null && EnvelopeCache.hasStartupCrashMarker(dirPath, options)) {
options
.getLogger()
.log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush.");
try {
future.get(options.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS);
romtsn marked this conversation as resolved.
Show resolved Hide resolved
} catch (TimeoutException e) {
// TODO: handle more exceptions
options
.getLogger()
.log(SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background.");
}
} else {
options
.getLogger()
.log(SentryLevel.DEBUG, "No Startup Crash marker exists, flushing asynchronously.");
}

options
.getLogger()
.log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed.");
Expand Down
Expand Up @@ -37,4 +37,8 @@ public SendFireAndForgetEnvelopeSender(

return processDir(envelopeSender, dirPath, options.getLogger());
}

@Override public @Nullable String getDirPath() {
return sendFireAndForgetDirPath.getDirPath();
}
}
Expand Up @@ -41,4 +41,8 @@ public SendFireAndForgetOutboxSender(

return processDir(outboxSender, dirPath, options.getLogger());
}

@Override public @Nullable String getDirPath() {
return sendFireAndForgetDirPath.getDirPath();
}
}
67 changes: 67 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Expand Up @@ -354,6 +354,31 @@ public class SentryOptions {
/** ClientReportRecorder to track count of lost events / transactions / ... * */
@NotNull IClientReportRecorder clientReportRecorder = new ClientReportRecorder(this);

/**
* Controls how many seconds to wait for sending events in case there were Startup Crashes in the
* previous run. Sentry SDKs normally send events from a background queue, but in the case of
* Startup Crashes, it blocks the execution of the {@link Sentry#init()} function for the amount
* of startupCrashFlushTimeoutMillis to make sure the events make it to Sentry.
*
* When the timeout is reached, the execution will continue on background.
*
* Default is 5000 = 5s.
*/
private long startupCrashFlushTimeoutMillis = 5000; // 5s

/**
* Controls the threshold after the application startup time, within which a crash should happen
* to be considered a Startup Crash.
*
* Startup Crashes are sent on @{@link Sentry#init()} in a blocking way, controlled by
* {@link SentryOptions#startupCrashFlushTimeoutMillis}.
*
* Default is 2000 = 2s.
*/
private long startupCrashDurationThresholdMillis = 2000; // 2s
romtsn marked this conversation as resolved.
Show resolved Hide resolved

private long sdkInitTime = 0;
romtsn marked this conversation as resolved.
Show resolved Hide resolved
romtsn marked this conversation as resolved.
Show resolved Hide resolved

/**
* Adds an event processor
*
Expand Down Expand Up @@ -1742,6 +1767,47 @@ public void setSendClientReports(boolean sendClientReports) {
return clientReportRecorder;
}

/**
* Returns the Startup Crash flush timeout in Millis
*
* @return the timeout in Millis
*/
public long getStartupCrashFlushTimeoutMillis() {
return startupCrashFlushTimeoutMillis;
}

/**
* Sets the Startup Crash flush timeout in Millis
*
* @param startupCrashFlushTimeoutMillis the timeout in Millis
*/
public void setStartupCrashFlushTimeoutMillis(long startupCrashFlushTimeoutMillis) {
this.startupCrashFlushTimeoutMillis = startupCrashFlushTimeoutMillis;
}

/**
* Returns the Startup Crash duration threshold in Millis
*
* @return the threshold in Millis
*/
public long getStartupCrashDurationThresholdMillis() {
return startupCrashDurationThresholdMillis;
}

/**
* Sets the Startup Crash duration threshold in Millis
*
* @param startupCrashDurationThresholdMillis the threshold in Millis
*/
public void setStartupCrashDurationThresholdMillis(long startupCrashDurationThresholdMillis) {
this.startupCrashDurationThresholdMillis = startupCrashDurationThresholdMillis;
}

@ApiStatus.Internal
public long getSdkInitTime() {
return sdkInitTime;
}

/** The BeforeSend callback */
public interface BeforeSendCallback {

Expand Down Expand Up @@ -1821,6 +1887,7 @@ public SentryOptions() {
*/
private SentryOptions(final boolean empty) {
if (!empty) {
sdkInitTime = System.currentTimeMillis();
romtsn marked this conversation as resolved.
Show resolved Hide resolved
// SentryExecutorService should be initialized before any
// SendCachedEventFireAndForgetIntegration
executorService = new SentryExecutorService();
Expand Down
37 changes: 37 additions & 0 deletions sentry/src/main/java/io/sentry/cache/EnvelopeCache.java
Expand Up @@ -58,6 +58,8 @@ public final class EnvelopeCache extends CacheStrategy implements IEnvelopeCache
public static final String CRASH_MARKER_FILE = "last_crash";
public static final String NATIVE_CRASH_MARKER_FILE = ".sentry-native/" + CRASH_MARKER_FILE;

public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash";
romtsn marked this conversation as resolved.
Show resolved Hide resolved

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

public static @NotNull IEnvelopeCache create(final @NotNull SentryOptions options) {
Expand Down Expand Up @@ -202,7 +204,42 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi
// write file to the disk when its about to crash so crashedLastRun can be marked on restart
if (HintUtils.hasType(hint, DiskFlushNotification.class)) {
writeCrashMarkerFile();

long timeSinceSdkInit = System.currentTimeMillis() - options.getSdkInitTime();
if (timeSinceSdkInit <= options.getStartupCrashDurationThresholdMillis()) {
options
.getLogger()
.log(DEBUG,
"Startup Crash detected %d milliseconds after SDK init. Writing a startup crash marker file to disk.",
timeSinceSdkInit);
writeStartupCrashMarkerFile();
}
}
}

private void writeStartupCrashMarkerFile() {
final File crashMarkerFile = new File(options.getCacheDirPath(), STARTUP_CRASH_MARKER_FILE);
try {
crashMarkerFile.createNewFile();
} catch (Throwable e) {
options.getLogger().log(ERROR, "Error writing the startup crash marker file to the disk", e);
}
}

public static boolean hasStartupCrashMarker(final @NotNull String dirPath, final @NotNull SentryOptions options) {
final File crashMarkerFile = new File(dirPath, STARTUP_CRASH_MARKER_FILE);
final boolean exists = crashMarkerFile.exists();
if (exists) {
if (!crashMarkerFile.delete()) {
romtsn marked this conversation as resolved.
Show resolved Hide resolved
romtsn marked this conversation as resolved.
Show resolved Hide resolved
options
.getLogger()
.log(
ERROR,
"Failed to delete the startup crash marker file. %s.",
crashMarkerFile.getAbsolutePath());
}
}
return exists;
}

private void writeCrashMarkerFile() {
Expand Down