Skip to content

Commit

Permalink
Merge cc8f21a into 4ca1d7b
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed Oct 11, 2022
2 parents 4ca1d7b + cc8f21a commit 485f4dd
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 11 deletions.
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,13 @@ 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 +189,8 @@ 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ final class ManifestMetadataReader {
static final String CLIENT_REPORTS_ENABLE = "io.sentry.send-client-reports";
static final String COLLECT_ADDITIONAL_CONTEXT = "io.sentry.additional-context";

static final String STARTUP_CRASH_FLUSH_TIMEOUT = "io.sentry.startup-crash.flush-timeout";

static final String STARTUP_CRASH_DURATION_THRESHOLD =
"io.sentry.startup-crash.duration-threshold";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -226,6 +231,20 @@ static void applyMetadata(
COLLECT_ADDITIONAL_CONTEXT,
options.isCollectAdditionalContext()));

options.setStartupCrashDurationThresholdMillis(
readLong(
metadata,
logger,
STARTUP_CRASH_DURATION_THRESHOLD,
options.getStartupCrashDurationThresholdMillis()));

options.setStartupCrashFlushTimeoutMillis(
readLong(
metadata,
logger,
STARTUP_CRASH_FLUSH_TIMEOUT,
options.getStartupCrashFlushTimeoutMillis()));

if (options.getTracesSampleRate() == null) {
final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE);
if (tracesSampleRate != -1) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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.");
}
} else {
androidOptions
.getLogger()
.log(SentryLevel.DEBUG, "No Startup Crash marker exists, flushing asynchronously.");
}

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 @@ -107,6 +107,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 long startupCrashDurationThresholdMillis = 2000; // 2s

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

/**
* 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.sentry.android.core.cache;

import static io.sentry.SentryLevel.DEBUG;
import static io.sentry.SentryLevel.ERROR;

import android.os.SystemClock;
import io.sentry.Hint;
import io.sentry.SentryEnvelope;
import io.sentry.SentryOptions;
import io.sentry.android.core.AppStartState;
import io.sentry.android.core.SentryAndroidOptions;
import io.sentry.cache.EnvelopeCache;
import io.sentry.hints.DiskFlushNotification;
import io.sentry.util.HintUtils;
import io.sentry.util.Objects;
import java.io.File;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

@ApiStatus.Internal
public final class AndroidEnvelopeCache extends EnvelopeCache {

public AndroidEnvelopeCache(final @NotNull SentryAndroidOptions options) {
super(
options,
Objects.requireNonNull(options.getCacheDirPath(), "cacheDirPath must not be null"),
options.getMaxCacheItems());
}

@Override
public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
super.store(envelope, hint);

final SentryAndroidOptions options = (SentryAndroidOptions) this.options;

final Long appStartTime = AppStartState.getInstance().getAppStartMillis();
if (HintUtils.hasType(hint, DiskFlushNotification.class) && appStartTime != null) {
long timeSinceSdkInit = SystemClock.uptimeMillis() - appStartTime;
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() {
// we use outbox path always, as it's the one that will also contain markers if hybrid sdks
// decide to write it, which will trigger the blocking init
final String outboxPath = options.getOutboxPath();
if (outboxPath == null) {
options.getLogger().log(DEBUG, "Outbox path is null, the startup crash marker file will not be written");
return;
}
final File crashMarkerFile = new File(options.getOutboxPath(), 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 SentryOptions options) { final String outboxPath = options.getOutboxPath();
if (outboxPath == null) {
options.getLogger().log(DEBUG, "Outbox path is null, the startup crash marker file does not exist");
return false;
}

final File crashMarkerFile = new File(options.getOutboxPath(), STARTUP_CRASH_MARKER_FILE);
try {
final boolean exists = crashMarkerFile.exists();
if (exists) {
if (!crashMarkerFile.delete()) {
options
.getLogger()
.log(
ERROR,
"Failed to delete the startup crash marker file. %s.",
crashMarkerFile.getAbsolutePath());
}
}
return exists;
} catch (Throwable e) {
options
.getLogger()
.log(ERROR, "Error reading/deleting the startup crash marker file on the disk", e);
}
return false;
}
}
4 changes: 3 additions & 1 deletion sentry/src/main/java/io/sentry/OutboxSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.sentry.SentryLevel.ERROR;
import static io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE;

import io.sentry.cache.EnvelopeCache;
import io.sentry.hints.Flushable;
import io.sentry.hints.Resettable;
import io.sentry.hints.Retryable;
Expand Down Expand Up @@ -96,7 +97,8 @@ protected void processFile(final @NotNull File file, @NotNull Hint hint) {
@Override
protected boolean isRelevantFileName(final @Nullable String fileName) {
// ignore current.envelope
return fileName != null && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE);
return fileName != null && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE) && !fileName.startsWith(
EnvelopeCache.STARTUP_CRASH_MARKER_FILE);
// TODO: Use an extension to filter out relevant files
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions
.log(SentryLevel.ERROR, "Failed trying to send cached events.", e);
}
});

options
.getLogger()
.log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed.");
Expand Down
Loading

0 comments on commit 485f4dd

Please sign in to comment.