Skip to content

Do not report cached events as lost #4575

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d0c782
Do not report cached events as lost
adinauer Jul 29, 2025
d521a60
E2E tests for OpenTelemetry based console sample (#4563)
adinauer Jul 29, 2025
a320001
Merge branch 'main' into 07-29-do_not_report_cached_events_as_lost
adinauer Jul 30, 2025
b435169
release: 8.18.0
getsentry-bot Jul 30, 2025
c1cee9b
ref(replay): Use main thread to schedule capture (#4542)
romtsn Jul 30, 2025
59250c4
perf(connectivity): Cache network capabilities and status to reduce I…
romtsn Jul 30, 2025
270a4c3
fix(breadcrumbs): Deduplicate battery breadcrumbs (#4561)
romtsn Jul 30, 2025
039ffbc
fix(ci): remove obsolete NDK debug symbols (#4581)
markushi Jul 31, 2025
3fd31b6
fix(android): Remove unused method (#4585)
markushi Jul 31, 2025
10e42ce
Add rules file for documenting SDK offline behaviour (#4572)
adinauer Aug 1, 2025
e45908a
perf(connectivity): Have only one NetworkCallback active at a time (#…
romtsn Aug 1, 2025
63b9d7b
fix(scripts): update-gradle script set-version (#4591)
romtsn Aug 1, 2025
0f2e1a2
fix: sentry-android-ndk proguard rule keeps all native class (#4427)
ghasemdev Aug 5, 2025
b1a54ca
refactor(lifecycle): Use single lifecycle observer (#4567)
romtsn Aug 5, 2025
29f057b
fix(sqlite): Fix abstract method error (#4597)
markushi Aug 5, 2025
d4ba02b
perf(integrations): Do not register for SystemEvents and NetworkCallb…
romtsn Aug 6, 2025
4425a1b
fix(android): Ensure frame metrics listeners are registered/unregiste…
markushi Aug 7, 2025
2c7271a
perf(executor): Prewarm SentryExecutorService (#4606)
romtsn Aug 7, 2025
644dcd8
review feedback
adinauer Aug 8, 2025
78bb822
changelog
adinauer Aug 8, 2025
c1a6766
Merge branch 'main' into 07-29-do_not_report_cached_events_as_lost
adinauer Aug 8, 2025
9ac34bb
pass through whether cache stored in AndroidEnvelopeCache + test
adinauer Aug 8, 2025
b4ac53c
Format code
getsentry-bot Aug 8, 2025
ffd313f
Merge branch 'main' into 07-29-do_not_report_cached_events_as_lost
adinauer Aug 8, 2025
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 @@ -26,6 +26,8 @@
```
- Fix abstract method error in `SentrySupportSQLiteDatabase` ([#4597](https://github.com/getsentry/sentry-java/pull/4597))
- Ensure frame metrics listeners are registered/unregistered on the main thread ([#4582](https://github.com/getsentry/sentry-java/pull/4582))
- Do not report cached events as lost ([#4575](https://github.com/getsentry/sentry-java/pull/4575))
- Previously events were recorded as lost early despite being retried later through the cache

## 8.18.0

Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
}

public class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapter : android/app/Application$ActivityLifecycleCallbacks {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,19 @@ public AndroidEnvelopeCache(final @NotNull SentryAndroidOptions options) {
this.currentDateProvider = currentDateProvider;
}

@SuppressWarnings("deprecation")
@Override
public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
super.store(envelope, hint);
storeInternalAndroid(envelope, hint);
}

@Override
public boolean storeEnvelope(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
return storeInternalAndroid(envelope, hint);
}

private boolean storeInternalAndroid(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
final boolean didStore = super.storeEnvelope(envelope, hint);

final SentryAndroidOptions options = (SentryAndroidOptions) this.options;
final TimeSpan sdkInitTimeSpan = AppStartMetrics.getInstance().getSdkInitTimeSpan();
Expand Down Expand Up @@ -83,6 +93,7 @@ public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {

writeLastReportedAnrMarker(timestamp);
});
return didStore;
}

@TestOnly
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.sentry.android.core.cache

import io.sentry.ISerializer
import io.sentry.NoOpLogger
import io.sentry.SentryEnvelope
import io.sentry.SentryOptions
import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint
import io.sentry.android.core.AnrV2Integration.AnrV2Hint
import io.sentry.android.core.SentryAndroidOptions
Expand All @@ -18,7 +20,9 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.same
import org.mockito.kotlin.whenever

class AndroidEnvelopeCacheTest {
Expand All @@ -35,8 +39,10 @@ class AndroidEnvelopeCacheTest {
dir: TemporaryFolder,
appStartMillis: Long? = null,
currentTimeMillis: Long? = null,
optionsCallback: ((SentryOptions) -> Unit)? = null,
): AndroidEnvelopeCache {
options.cacheDirPath = dir.newFolder("sentry-cache").absolutePath
optionsCallback?.invoke(options)
val outboxDir = File(options.outboxPath!!)
outboxDir.mkdirs()

Expand Down Expand Up @@ -82,7 +88,7 @@ class AndroidEnvelopeCacheTest {
val cache = fixture.getSut(tmpDir)

val hints = HintUtils.createWithTypeCheckHint(UncaughtHint())
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertFalse(fixture.startupCrashMarkerFile.exists())
}
Expand All @@ -92,7 +98,7 @@ class AndroidEnvelopeCacheTest {
val cache = fixture.getSut(dir = tmpDir, appStartMillis = 1000L, currentTimeMillis = 5000L)

val hints = HintUtils.createWithTypeCheckHint(UncaughtHint())
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertFalse(fixture.startupCrashMarkerFile.exists())
}
Expand All @@ -104,7 +110,7 @@ class AndroidEnvelopeCacheTest {
fixture.options.cacheDirPath = null

val hints = HintUtils.createWithTypeCheckHint(UncaughtHint())
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertFalse(fixture.startupCrashMarkerFile.exists())
}
Expand All @@ -114,7 +120,7 @@ class AndroidEnvelopeCacheTest {
val cache = fixture.getSut(dir = tmpDir, appStartMillis = 1000L, currentTimeMillis = 2000L)

val hints = HintUtils.createWithTypeCheckHint(UncaughtHint())
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertTrue(fixture.startupCrashMarkerFile.exists())
}
Expand All @@ -138,7 +144,7 @@ class AndroidEnvelopeCacheTest {
HintUtils.createWithTypeCheckHint(
AnrV2Hint(0, NoOpLogger.getInstance(), 12345678L, false, false)
)
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertFalse(fixture.lastReportedAnrFile.exists())
}
Expand All @@ -151,7 +157,7 @@ class AndroidEnvelopeCacheTest {
HintUtils.createWithTypeCheckHint(
AnrV2Hint(0, NoOpLogger.getInstance(), 12345678L, false, false)
)
cache.store(fixture.envelope, hints)
cache.storeEnvelope(fixture.envelope, hints)

assertTrue(fixture.lastReportedAnrFile.exists())
assertEquals("12345678", fixture.lastReportedAnrFile.readText())
Expand Down Expand Up @@ -189,5 +195,17 @@ class AndroidEnvelopeCacheTest {
assertEquals(87654321L, lastReportedAnr)
}

@Test
fun `returns false if storing fails`() {
val serializer = mock<ISerializer>()
val cache = fixture.getSut(tmpDir) { options -> options.setSerializer(serializer) }
whenever(serializer.serialize(same(fixture.envelope), any()))
.thenThrow(RuntimeException("forced ex"))
val hints = HintUtils.createWithTypeCheckHint(UncaughtHint())

val didStore = cache.storeEnvelope(fixture.envelope, hints)
assertFalse(didStore)
}

internal class UncaughtHint : UncaughtExceptionHint(0, NoOpLogger.getInstance())
}
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -4354,13 +4354,15 @@ public class io/sentry/cache/EnvelopeCache : io/sentry/cache/IEnvelopeCache {
public static fun getPreviousSessionFile (Ljava/lang/String;)Ljava/io/File;
public fun iterator ()Ljava/util/Iterator;
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
public fun waitPreviousSessionFlush ()Z
}

public abstract interface class io/sentry/cache/IEnvelopeCache : java/lang/Iterable {
public abstract fun discard (Lio/sentry/SentryEnvelope;)V
public fun store (Lio/sentry/SentryEnvelope;)V
public abstract fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
}

public final class io/sentry/cache/PersistingOptionsObserver : io/sentry/IOptionsObserver {
Expand Down Expand Up @@ -6663,6 +6665,7 @@ public final class io/sentry/transport/NoOpEnvelopeCache : io/sentry/cache/IEnve
public static fun getInstance ()Lio/sentry/transport/NoOpEnvelopeCache;
public fun iterator ()Ljava/util/Iterator;
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/transport/NoOpTransport : io/sentry/transport/ITransport {
Expand Down
19 changes: 16 additions & 3 deletions sentry/src/main/java/io/sentry/cache/EnvelopeCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,18 @@ public EnvelopeCache(
previousSessionLatch = new CountDownLatch(1);
}

@SuppressWarnings("deprecation")
@Override
public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) {
storeInternal(envelope, hint);
}

@Override
public boolean storeEnvelope(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) {
return storeInternal(envelope, hint);
}

private boolean storeInternal(final @NotNull SentryEnvelope envelope, final @NotNull Hint hint) {
Objects.requireNonNull(envelope, "Envelope is required.");

rotateCacheIfNeeded(allEnvelopeFiles());
Expand Down Expand Up @@ -171,19 +181,20 @@ public void store(final @NotNull SentryEnvelope envelope, final @NotNull Hint hi
WARNING,
"Not adding Envelope to offline storage because it already exists: %s",
envelopeFile.getAbsolutePath());
return;
return true;
} else {
options
.getLogger()
.log(DEBUG, "Adding Envelope to offline storage: %s", envelopeFile.getAbsolutePath());
}

writeEnvelopeToDisk(envelopeFile, envelope);
final boolean didWriteToDisk = writeEnvelopeToDisk(envelopeFile, envelope);

// write file to the disk when its about to crash so crashedLastRun can be marked on restart
if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) {
writeCrashMarkerFile();
}
return didWriteToDisk;
}

/**
Expand Down Expand Up @@ -295,7 +306,7 @@ private void updateCurrentSession(
}
}

private void writeEnvelopeToDisk(
private boolean writeEnvelopeToDisk(
final @NotNull File file, final @NotNull SentryEnvelope envelope) {
if (file.exists()) {
options
Expand All @@ -312,7 +323,9 @@ private void writeEnvelopeToDisk(
options
.getLogger()
.log(ERROR, e, "Error writing Envelope %s to offline storage", file.getAbsolutePath());
return false;
}
return true;
}

private void writeSessionToDisk(final @NotNull File file, final @NotNull Session session) {
Expand Down
9 changes: 8 additions & 1 deletion sentry/src/main/java/io/sentry/cache/IEnvelopeCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@

public interface IEnvelopeCache extends Iterable<SentryEnvelope> {

@Deprecated
void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint);

default boolean storeEnvelope(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you also explicitly implement it in AndroidEnvelopeCache? I recall there were some issues with desugaring on Unity, where it would crash with NoSuchMethodError for default methods in core sentry

store(envelope, hint);
return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should actually return false in case an envelope couldn't be stored here, wdyt? Could be because of no storage space, for example.

}

@Deprecated
default void store(@NotNull SentryEnvelope envelope) {
store(envelope, new Hint());
storeEnvelope(envelope, new Hint());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Cache Store Failure Masking

The default implementation of IEnvelopeCache.storeEnvelope() always returns true, even if a custom implementation of the deprecated store(SentryEnvelope, Hint) method fails silently. This incorrectly indicates successful caching. Furthermore, the deprecated store(SentryEnvelope) method calls storeEnvelope() but ignores its boolean return value, masking storage failures for callers still using this API.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to maintain backward compatibility and thus cannot replace the method directly. We are overriding storeEnvelope in both EnvelopeCache and AndroidEnvelopeCache to avoid this problem.

}

void discard(@NotNull SentryEnvelope envelope);
Expand Down
43 changes: 25 additions & 18 deletions sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ private static QueuedThreadPoolExecutor initExecutor(
final EnvelopeSender envelopeSender = (EnvelopeSender) r;

if (!HintUtils.hasType(envelopeSender.hint, Cached.class)) {
envelopeCache.store(envelopeSender.envelope, envelopeSender.hint);
envelopeCache.storeEnvelope(envelopeSender.envelope, envelopeSender.hint);
}

markHintWhenSendingFailed(envelopeSender.hint, true);
Expand Down Expand Up @@ -271,7 +271,7 @@ public void run() {
TransportResult result = this.failedResult;

envelope.getHeader().setSentAt(null);
envelopeCache.store(envelope, hint);
boolean cached = envelopeCache.storeEnvelope(envelope, hint);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since envelopes that were cached previously have a no-op envelope cache here inEnvelopeSender, this will be false for envelopes sent from cache.


HintUtils.runIfHasType(
hint,
Expand Down Expand Up @@ -311,14 +311,17 @@ public void run() {

// ignore e.g. 429 as we're not the ones actively dropping
if (result.getResponseCode() >= 400 && result.getResponseCode() != 429) {
HintUtils.runIfDoesNotHaveType(
hint,
Retryable.class,
(hint) -> {
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
});
if (!cached) {
HintUtils.runIfDoesNotHaveType(
hint,
Retryable.class,
(hint) -> {
options
.getClientReportRecorder()
.recordLostEnvelope(
DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Envelope Cache Logic Fails

The NoOpEnvelopeCache.storeEnvelope() method always returns false. This method is called for envelopes that are already cached (indicated by a Cached hint). In AsyncHttpTransport, the error handling logic uses if (!cached) to determine if an envelope should be reported as lost. As a result, already-cached envelopes are incorrectly reported as lost when sending fails, directly contradicting the PR's goal to "Do not report cached events as lost."

Locations (5)
Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Envelopes that have already been cached will have the Retryable hint and thus not report the envelope as lost.

}

throw new IllegalStateException(message);
Expand All @@ -332,10 +335,12 @@ public void run() {
retryable.setRetry(true);
},
(hint, clazz) -> {
LogUtils.logNotInstanceOf(clazz, hint, options.getLogger());
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
if (!cached) {
LogUtils.logNotInstanceOf(clazz, hint, options.getLogger());
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
}
});
throw new IllegalStateException("Sending the event failed.", e);
}
Expand All @@ -348,10 +353,12 @@ public void run() {
retryable.setRetry(true);
},
(hint, clazz) -> {
LogUtils.logNotInstanceOf(clazz, hint, options.getLogger());
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope);
if (!cached) {
LogUtils.logNotInstanceOf(clazz, hint, options.getLogger());
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope);
}
});
}
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ public static NoOpEnvelopeCache getInstance() {
return instance;
}

@SuppressWarnings("deprecation")
@Override
public void store(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {}

@Override
public boolean storeEnvelope(@NotNull SentryEnvelope envelope, @NotNull Hint hint) {
return false;
}

@Override
public void discard(@NotNull SentryEnvelope envelope) {}

Expand Down
Loading
Loading