Skip to content

Commit

Permalink
Report Startup Crashes (#2277)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed Oct 17, 2022
1 parent afa17ae commit a4fb390
Show file tree
Hide file tree
Showing 20 changed files with 670 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- Add support for using Encoder with logback.SentryAppender ([#2246](https://github.com/getsentry/sentry-java/pull/2246))
- Add captureProfile method to hub and client ([#2290](https://github.com/getsentry/sentry-java/pull/2290))
- Report Startup Crashes ([#2277](https://github.com/getsentry/sentry-java/pull/2277))

## 6.5.0

Expand Down
10 changes: 10 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In

public final class io/sentry/android/core/AppStartState {
public fun getAppStartInterval ()Ljava/lang/Long;
public fun getAppStartMillis ()Ljava/lang/Long;
public fun getAppStartTime ()Ljava/util/Date;
public static fun getInstance ()Lio/sentry/android/core/AppStartState;
public fun isColdStart ()Ljava/lang/Boolean;
public fun reset ()V
public fun setAppStartMillis (J)V
}

public final class io/sentry/android/core/BuildConfig {
Expand Down Expand Up @@ -128,6 +131,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader;
public fun getProfilingTracesHz ()I
public fun getProfilingTracesIntervalMillis ()I
public fun getStartupCrashDurationThresholdMillis ()J
public fun isAnrEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachScreenshot ()Z
Expand Down Expand Up @@ -216,3 +220,9 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a
public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V
}

public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import android.content.res.AssetManager;
import android.os.Build;
import io.sentry.ILogger;
import io.sentry.SendCachedEnvelopeFireAndForgetIntegration;
import io.sentry.SendFireAndForgetEnvelopeSender;
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.util.Objects;
Expand Down Expand Up @@ -132,6 +132,7 @@ static void init(

ManifestMetadataReader.applyMetadata(context, options, buildInfoProvider);
initializeCacheDirs(context, options);
options.setEnvelopeDiskCache(new AndroidEnvelopeCache(options));

final ActivityFramesTracker activityFramesTracker =
new ActivityFramesTracker(loadClass, options.getLogger());
Expand Down Expand Up @@ -164,9 +165,14 @@ private static void installDefaultIntegrations(
final boolean isFragmentAvailable,
final boolean isTimberAvailable) {

// read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope
// integrations below
final boolean hasStartupCrashMarker = AndroidEnvelopeCache.hasStartupCrashMarker(options);

options.addIntegration(
new SendCachedEnvelopeFireAndForgetIntegration(
new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath())));
new SendCachedEnvelopeIntegration(
new SendFireAndForgetEnvelopeSender(() -> options.getCacheDirPath()),
hasStartupCrashMarker));

// Integrations are registered in the same order. NDK before adding Watch outbox,
// because sentry-native move files around and we don't want to watch that.
Expand All @@ -184,8 +190,9 @@ private static void installDefaultIntegrations(
// this should be executed after NdkIntegration because sentry-native move files on init.
// and we'd like to send them right away
options.addIntegration(
new SendCachedEnvelopeFireAndForgetIntegration(
new SendFireAndForgetOutboxSender(() -> options.getOutboxPath())));
new SendCachedEnvelopeIntegration(
new SendFireAndForgetOutboxSender(() -> options.getOutboxPath()),
hasStartupCrashMarker));

options.addIntegration(new AnrIntegration(context));
options.addIntegration(new AppLifecycleIntegration());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public Date getAppStartTime() {
return appStartTime;
}

@Nullable
public Long getAppStartMillis() {
return appStartMillis;
}

synchronized void setAppStartTime(final long appStartMillis, final @NotNull Date appStartTime) {
// method is synchronized because the SDK may by init. on a background thread.
if (this.appStartTime != null && this.appStartMillis != null) {
Expand All @@ -92,4 +97,16 @@ synchronized void setAppStartTime(final long appStartMillis, final @NotNull Date
this.appStartTime = appStartTime;
this.appStartMillis = appStartMillis;
}

@TestOnly
public synchronized void setAppStartMillis(final long appStartMillis) {
this.appStartMillis = appStartMillis;
}

@TestOnly
public synchronized void reset() {
appStartTime = null;
appStartMillis = null;
appStartEndMillis = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.sentry.android.core;

import io.sentry.IHub;
import io.sentry.Integration;
import io.sentry.SendCachedEnvelopeFireAndForgetIntegration;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.util.Objects;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.jetbrains.annotations.NotNull;

final class SendCachedEnvelopeIntegration implements Integration {

private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory
factory;
private final boolean hasStartupCrashMarker;

public SendCachedEnvelopeIntegration(
final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory,
final boolean hasStartupCrashMarker) {
this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required");
this.hasStartupCrashMarker = hasStartupCrashMarker;
}

@Override
public void register(@NotNull IHub hub, @NotNull SentryOptions options) {
Objects.requireNonNull(hub, "Hub is required");
final SentryAndroidOptions androidOptions =
Objects.requireNonNull(
(options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null,
"SentryAndroidOptions is required");

final String cachedDir = options.getCacheDirPath();
if (!factory.hasValidPath(cachedDir, options.getLogger())) {
options.getLogger().log(SentryLevel.ERROR, "No cache dir path is defined in options.");
return;
}

final SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender =
factory.create(hub, androidOptions);

if (sender == null) {
androidOptions.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null.");
return;
}

try {
Future<?> future =
androidOptions
.getExecutorService()
.submit(
() -> {
try {
sender.send();
} catch (Throwable e) {
androidOptions
.getLogger()
.log(SentryLevel.ERROR, "Failed trying to send cached events.", e);
}
});

if (hasStartupCrashMarker) {
androidOptions
.getLogger()
.log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush.");
try {
future.get(androidOptions.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
androidOptions
.getLogger()
.log(SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background.");
}
}

androidOptions.getLogger().log(SentryLevel.DEBUG, "SendCachedEnvelopeIntegration installed.");
} catch (Throwable e) {
androidOptions
.getLogger()
.log(SentryLevel.ERROR, "Failed to call the executor. Cached events will not be sent", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.transport.NoOpEnvelopeCache;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;
Expand Down Expand Up @@ -104,6 +106,7 @@ public static synchronized void init(
options, context, logger, isFragmentAvailable, isTimberAvailable);
configuration.configure(options);
deduplicateIntegrations(options, isFragmentAvailable, isTimberAvailable);
resetEnvelopeCacheIfNeeded(options);
},
true);
} catch (IllegalAccessException e) {
Expand Down Expand Up @@ -168,4 +171,18 @@ private static void deduplicateIntegrations(
}
}
}

/**
* Resets envelope cache if {@link SentryOptions#getCacheDirPath()} was set to null by the user
* and the IEnvelopCache implementation remained ours (AndroidEnvelopeCache), which relies on
* cacheDirPath set.
*
* @param options SentryOptions to retrieve cacheDirPath from
*/
private static void resetEnvelopeCacheIfNeeded(final @NotNull SentryAndroidOptions options) {
if (options.getCacheDirPath() == null
&& options.getEnvelopeDiskCache() instanceof AndroidEnvelopeCache) {
options.setEnvelopeDiskCache(NoOpEnvelopeCache.getInstance());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.sentry.protocol.SdkVersion;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;

/** Sentry SDK options for Android */
public final class SentryAndroidOptions extends SentryOptions {
Expand Down Expand Up @@ -107,6 +108,29 @@ public final class SentryAndroidOptions extends SentryOptions {
*/
private boolean collectAdditionalContext = true;

/**
* 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.
*
* <p>When the timeout is reached, the execution will continue on background.
*
* <p>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.
*
* <p>Startup Crashes are sent on {@link Sentry#init()} in a blocking way, controlled by {@link
* SentryAndroidOptions#startupCrashFlushTimeoutMillis}.
*
* <p>Default is 2000 = 2s.
*/
private final long startupCrashDurationThresholdMillis = 2000; // 2s

public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
Expand Down Expand Up @@ -332,4 +356,34 @@ public boolean isCollectAdditionalContext() {
public void setCollectAdditionalContext(boolean collectAdditionalContext) {
this.collectAdditionalContext = collectAdditionalContext;
}

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

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

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

0 comments on commit a4fb390

Please sign in to comment.