Skip to content

Commit

Permalink
Merge 5abf2b0 into 4ca1d7b
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn committed Oct 11, 2022
2 parents 4ca1d7b + 5abf2b0 commit 98043eb
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 10 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
10 changes: 9 additions & 1 deletion sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.sentry;

import io.sentry.cache.EnvelopeCache;
import io.sentry.cache.IEnvelopeCache;
import io.sentry.config.PropertiesProviderFactory;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.util.FileUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -230,7 +232,13 @@ private static boolean initConfigurations(final @NotNull SentryOptions options)
if (cacheDirPath != null) {
final File cacheDir = new File(cacheDirPath);
cacheDir.mkdirs();
options.setEnvelopeDiskCache(EnvelopeCache.create(options));
final IEnvelopeCache envelopeCache = options.getEnvelopeDiskCache();
// only overwrite the cache impl if it's not set by Android
if (envelopeCache instanceof NoOpEnvelopeCache) {
options.setEnvelopeDiskCache(EnvelopeCache.create(options));
}
} else {
options.setEnvelopeDiskCache(NoOpEnvelopeCache.getInstance());
}

final String profilingTracesDirPath = options.getProfilingTracesDirPath();
Expand Down
Loading

0 comments on commit 98043eb

Please sign in to comment.