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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
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))
romtsn marked this conversation as resolved.
Show resolved Hide resolved

## 6.5.0

Expand Down
10 changes: 10 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
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
}

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
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;
}
}
@@ -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);
}
}
}
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());
}
}
}
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;
}
}