Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

### Behavioral Changes

- Android: Raise the default `maxCacheItems` from 30 to 100, so offline devices keep more envelopes. `maxQueueSize` also defaults to 100 on Android to match it ([#6608](https://github.com/getsentry/sentry-java/pull/6608))
- Android: After a startup crash, the crash envelope is sent before the rest of the offline cache, and `Sentry.init` only blocks until that envelope is sent ([#6608](https://github.com/getsentry/sentry-java/pull/6608))
- Nav3: Returning to an Activity or Fragment with its own `SentryNavEffect` within the app (e.g., closing the Activity that covered it, swiping back to its ViewPager2 page, or dismissing a dialog over it) now starts a new navigation transaction for the current destination if the previous one finished, so spans produced upon return can now be consistently captured ([#6482](https://github.com/getsentry/sentry-java/pull/6482))
- Nav3: `ui.load` transactions are bound to `IScope` in preference to Nav3 transactions when transitioning between Activities, matching Nav2 behavior (API 29+) ([#6484](https://github.com/getsentry/sentry-java/pull/6484))

Expand Down
9 changes: 8 additions & 1 deletion sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -783,17 +783,24 @@ public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry
public static final field LAST_TOMBSTONE_REPORT Ljava/lang/String;
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun getDirectory ()Ljava/io/File;
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public static fun lastReportedMemoryLimiter (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public static fun lastReportedTombstone (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public static fun markAnrReported (Lio/sentry/SentryOptions;J)V
public static fun markMemoryLimiterReported (Lio/sentry/SentryOptions;J)V
public static fun markTombstoneReported (Lio/sentry/SentryOptions;J)V
public static fun readStartupCrashMarker (Lio/sentry/SentryOptions;)Lio/sentry/android/core/cache/AndroidEnvelopeCache$StartupCrashMarker;
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
}

public final class io/sentry/android/core/cache/AndroidEnvelopeCache$StartupCrashMarker {
public static final field NONE Lio/sentry/android/core/cache/AndroidEnvelopeCache$StartupCrashMarker;
public fun <init> (Ljava/lang/String;)V
public fun exists ()Z
public fun getEnvelopeFileName ()Ljava/lang/String;
}

public abstract class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter : android/app/Application$ActivityLifecycleCallbacks {
public fun <init> ()V
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,8 @@ static void installDefaultIntegrations(

// read the startup crash marker here to avoid doing double-IO for the SendCachedEnvelope
// integrations below
LazyEvaluator<Boolean> startupCrashMarkerEvaluator =
new LazyEvaluator<>(() -> AndroidEnvelopeCache.hasStartupCrashMarker(options));
LazyEvaluator<AndroidEnvelopeCache.StartupCrashMarker> startupCrashMarkerEvaluator =
new LazyEvaluator<>(() -> AndroidEnvelopeCache.readStartupCrashMarker(options));

options.addIntegration(
new SendCachedEnvelopeIntegration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@
import io.sentry.SendCachedEnvelopeFireAndForgetIntegration;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.cache.AndroidEnvelopeCache.StartupCrashMarker;
import io.sentry.transport.RateLimiter;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.LazyEvaluator;
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -29,7 +30,7 @@ final class SendCachedEnvelopeIntegration

private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory
factory;
private final @NotNull LazyEvaluator<Boolean> startupCrashMarkerEvaluator;
private final @NotNull LazyEvaluator<StartupCrashMarker> startupCrashMarkerEvaluator;
private final AtomicBoolean startupCrashHandled = new AtomicBoolean(false);
private @Nullable IConnectionStatusProvider connectionStatusProvider;
private @Nullable IScopes scopes;
Expand All @@ -41,7 +42,7 @@ final class SendCachedEnvelopeIntegration

public SendCachedEnvelopeIntegration(
final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory,
final @NotNull LazyEvaluator<Boolean> startupCrashMarkerEvaluator) {
final @NotNull LazyEvaluator<StartupCrashMarker> startupCrashMarkerEvaluator) {
this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required");
this.startupCrashMarkerEvaluator = startupCrashMarkerEvaluator;
}
Expand Down Expand Up @@ -86,6 +87,17 @@ public void onConnectionStatusChanged(
private void sendCachedEnvelopes(
final @NotNull IScopes scopes, final @NotNull SentryAndroidOptions options) {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
// startupCrashMarkerEvaluator remains the same on subsequent runs, let's ensure we only block
// on the very first execution (=app start)
final @NotNull StartupCrashMarker startupCrashMarker = startupCrashMarkerEvaluator.getValue();
final boolean blockOnStartupCrash =
startupCrashMarker.exists() && startupCrashHandled.compareAndSet(false, true);
final @Nullable String startupCrashFileName =
blockOnStartupCrash ? startupCrashMarker.getEnvelopeFileName() : null;
// released once the startup crash envelope is sent, or once the whole directory is sent if
// this sender can't find it
final CountDownLatch startupCrashSent = new CountDownLatch(1);

final Future<?> future =
options
.getExecutorService()
Expand Down Expand Up @@ -138,25 +150,30 @@ private void sendCachedEnvelopes(
return;
}

if (startupCrashFileName != null && sender.sendFile(startupCrashFileName)) {
startupCrashSent.countDown();
}
sender.send();
} catch (Throwable e) {
options
.getLogger()
.log(SentryLevel.ERROR, "Failed trying to send cached events.", e);
} finally {
startupCrashSent.countDown();
}
});

// startupCrashMarkerEvaluator remains true on subsequent runs, let's ensure we only block on
// the very first execution (=app start)
if (startupCrashMarkerEvaluator.getValue()
&& startupCrashHandled.compareAndSet(false, true)) {
// a cancelled task never runs, so there's nothing to wait for
if (blockOnStartupCrash && !future.isCancelled()) {
options.getLogger().log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush.");
try {
future.get(options.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
options
.getLogger()
.log(SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background.");
if (!startupCrashSent.await(
options.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS)) {
options
.getLogger()
.log(
SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background.");
}
} catch (InterruptedException e) {
// We don't own the calling thread, so keep its interrupt for the code that does
Thread.currentThread().interrupt();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
/** Sentry SDK options for Android */
public final class SentryAndroidOptions extends SentryOptions {

private static final int DEFAULT_MAX_CACHE_ITEMS = 100;

/**
* Enable or disable ANR (Application Not Responding) Default is enabled Used by AnrIntegration
*/
Expand Down Expand Up @@ -337,6 +339,10 @@ public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
setAttachServerName(false);
// devices are offline more often than servers, so keep more envelopes on disk. The queue size
// matches it, so the transport queue and the dedupe of in-flight cached files fit the cache
setMaxCacheItems(DEFAULT_MAX_CACHE_ITEMS);
setMaxQueueSize(DEFAULT_MAX_CACHE_ITEMS);
}

private @NotNull SdkVersion createSdkVersion() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -84,7 +85,7 @@ private boolean storeInternalAndroid(@NotNull SentryEnvelope envelope, @NotNull
DEBUG,
"Startup Crash detected %d milliseconds after SDK init. Writing a startup crash marker file to disk.",
timeSinceSdkInit);
writeStartupCrashMarkerFile();
writeStartupCrashMarkerFile(getEnvelopeFile(envelope).getName());
}
}

Expand All @@ -100,7 +101,11 @@ private boolean storeInternalAndroid(@NotNull SentryEnvelope envelope, @NotNull
return directory.getFile();
}

private void writeStartupCrashMarkerFile() {
/**
* Writes the startup crash marker, which names the cached crash envelope so the next app start
* can send it ahead of the rest of the cache.
*/
private void writeStartupCrashMarkerFile(final @NotNull String envelopeFileName) {
// 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();
Expand All @@ -118,42 +123,86 @@ private void writeStartupCrashMarkerFile() {
return;
}
final File crashMarkerFile = new File(outboxDir, STARTUP_CRASH_MARKER_FILE);
try {
crashMarkerFile.createNewFile();
try (final OutputStream outputStream = new FileOutputStream(crashMarkerFile)) {
outputStream.write(envelopeFileName.getBytes(UTF_8));
} 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) {
/**
* Reads and deletes the startup crash marker.
*
* @return the marker, {@link StartupCrashMarker#NONE} if there is none
*/
public static @NotNull StartupCrashMarker readStartupCrashMarker(
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;
return StartupCrashMarker.NONE;
}

final File crashMarkerFile = new File(outboxPath, 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());
}
if (!crashMarkerFile.exists()) {
return StartupCrashMarker.NONE;
}
return exists;
@Nullable String content = null;
try {
content = FileUtils.readText(crashMarkerFile);
} catch (IOException e) {
options.getLogger().log(ERROR, "Error reading the startup crash marker file", e);
}
if (!crashMarkerFile.delete()) {
options
.getLogger()
.log(
ERROR,
"Failed to delete the startup crash marker file. %s.",
crashMarkerFile.getAbsolutePath());
}
// older SDK versions, and possibly hybrid SDKs, write an empty marker
final @Nullable String envelopeFileName = content == null ? null : content.trim();
return new StartupCrashMarker(
envelopeFileName == null || envelopeFileName.isEmpty() ? null : envelopeFileName);
} catch (Throwable e) {
options
.getLogger()
.log(ERROR, "Error reading/deleting the startup crash marker file on the disk", e);
}
return false;
return StartupCrashMarker.NONE;
}

/** The startup crash marker of the previous app run, read once on app start. */
public static final class StartupCrashMarker {
public static final @NotNull StartupCrashMarker NONE = new StartupCrashMarker(false, null);

private final boolean exists;
private final @Nullable String envelopeFileName;

/**
* @param envelopeFileName the file name of the crash envelope in the cache dir, or null if the
* marker doesn't name one
*/
public StartupCrashMarker(final @Nullable String envelopeFileName) {
this(true, envelopeFileName);
}

private StartupCrashMarker(final boolean exists, final @Nullable String envelopeFileName) {
this.exists = exists;
this.envelopeFileName = envelopeFileName;
}

public boolean exists() {
return exists;
}

public @Nullable String getEnvelopeFileName() {
return envelopeFileName;
}
}

private static @Nullable Long lastReportedMarker(
Expand Down
Loading
Loading