From 5b39f0d528d7fab34718f903b03a9a3748648637 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 4 Mar 2026 00:14:07 +0100 Subject: [PATCH 1/5] feat: Add public API to retrieve persisted breadcrumbs from Android SDK Reads breadcrumbs from the Android SDK's PersistingScopeObserver during init and exposes them via SentrySdk.GetPersistedBreadcrumbs(). This allows customers to access breadcrumbs from the previous app session without needing access to internal binding types. Co-Authored-By: Claude Opus 4.6 --- src/Sentry/Platforms/Android/SentrySdk.cs | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 919ec00357..d1e280549f 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -1,5 +1,6 @@ using Android.Content.PM; using Android.OS; +using Android.Runtime; using Sentry.Android; using Sentry.Android.Callbacks; using Sentry.Android.Extensions; @@ -22,6 +23,7 @@ namespace Sentry; public static partial class SentrySdk { private static AndroidContext AppContext { get; set; } = Application.Context; + private static List? _persistedBreadcrumbs; private static void InitSentryAndroidSdk(SentryOptions options) { @@ -184,6 +186,9 @@ private static void InitSentryAndroidSdk(SentryOptions options) SentryAndroid.Init(AppContext, configuration); } + // Read persisted breadcrumbs from the previous session before they get overwritten + _persistedBreadcrumbs = ReadPersistedBreadcrumbs(nativeOptions!); + // Set options for the managed SDK that depend on the Android SDK. (The user will not be able to modify these.) options.AddEventProcessor(new AndroidEventProcessor(nativeOptions!)); if (options.Android.LogCatIntegration != LogCatIntegrationType.None) @@ -286,4 +291,39 @@ private static void AndroidEnvironment_UnhandledExceptionRaiser(object? _, Raise #pragma warning restore CA1422 #pragma warning restore CS0618 } + + /// + /// Retrieves breadcrumbs that were persisted to disk by the Android SDK's scope observer. + /// These are breadcrumbs from the previous app session that were saved before the app terminated. + /// + /// A list of persisted breadcrumbs, or an empty list if unavailable. + public static IReadOnlyList GetPersistedBreadcrumbs() => + _persistedBreadcrumbs ?? []; + + private static List? ReadPersistedBreadcrumbs(JavaSdk.SentryOptions nativeOptions) + { + var observer = nativeOptions.FindPersistingScopeObserver(); + if (observer is null) + { + return null; + } + + var result = observer.Read(nativeOptions, "breadcrumbs.json", + Java.Lang.Class.FromType(typeof(Java.Util.ArrayList))); + + if (result is not Java.Util.IList javaList) + { + return null; + } + + var breadcrumbs = new List(); + for (var i = 0; i < javaList.Size(); i++) + { + if (javaList.Get(i)?.JavaCast() is { } javaBreadcrumb) + { + breadcrumbs.Add(javaBreadcrumb.ToBreadcrumb()); + } + } + return breadcrumbs; + } } From b4462d6fbe83b307783d4eaa3228e4818609cc61 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 4 Mar 2026 00:17:52 +0100 Subject: [PATCH 2/5] changelog: Add entry for GetPersistedBreadcrumbs API Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6a15ac40..7bfa2cc793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add `SentrySdk.GetPersistedBreadcrumbs()` API to retrieve breadcrumbs from the previous Android app session ([#4977](https://github.com/getsentry/sentry-dotnet/pull/4977)) + ### Fixes - The SDK now logs a `Warning` instead of an `Error` when being ratelimited ([#4927](https://github.com/getsentry/sentry-dotnet/pull/4927)) From ea4162410695ed2a851dce76eb0a562b22291e8e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 4 Mar 2026 11:58:15 +0100 Subject: [PATCH 3/5] fix: Use IList instead of ArrayList for persisted breadcrumbs read The Java SDK returns an unmodifiable list (Collections$UnmodifiableRandomAccessList) which cannot be cast to ArrayList. Co-Authored-By: Claude Opus 4.6 --- src/Sentry/Platforms/Android/SentrySdk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index d1e280549f..ca5ca5f040 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -309,7 +309,7 @@ public static IReadOnlyList GetPersistedBreadcrumbs() => } var result = observer.Read(nativeOptions, "breadcrumbs.json", - Java.Lang.Class.FromType(typeof(Java.Util.ArrayList))); + Java.Lang.Class.FromType(typeof(Java.Util.IList))); if (result is not Java.Util.IList javaList) { From 43f43d86c0bba18cbe5fc3dbd1c959d01b87dad8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 4 Mar 2026 13:45:02 +0100 Subject: [PATCH 4/5] fix: Catch Java exceptions when reading persisted breadcrumbs The scope observer list may be concurrently modified during init, causing a ConcurrentModificationException. Gracefully return null instead of crashing. Co-Authored-By: Claude Opus 4.6 --- src/Sentry/Platforms/Android/SentrySdk.cs | 39 +++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index ca5ca5f040..110381a2fa 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -302,28 +302,35 @@ public static IReadOnlyList GetPersistedBreadcrumbs() => private static List? ReadPersistedBreadcrumbs(JavaSdk.SentryOptions nativeOptions) { - var observer = nativeOptions.FindPersistingScopeObserver(); - if (observer is null) + try { - return null; - } + var observer = nativeOptions.FindPersistingScopeObserver(); + if (observer is null) + { + return null; + } - var result = observer.Read(nativeOptions, "breadcrumbs.json", - Java.Lang.Class.FromType(typeof(Java.Util.IList))); + var result = observer.Read(nativeOptions, "breadcrumbs.json", + Java.Lang.Class.FromType(typeof(Java.Util.IList))); - if (result is not Java.Util.IList javaList) - { - return null; - } + if (result is not Java.Util.IList javaList) + { + return null; + } - var breadcrumbs = new List(); - for (var i = 0; i < javaList.Size(); i++) - { - if (javaList.Get(i)?.JavaCast() is { } javaBreadcrumb) + var breadcrumbs = new List(); + for (var i = 0; i < javaList.Size(); i++) { - breadcrumbs.Add(javaBreadcrumb.ToBreadcrumb()); + if (javaList.Get(i)?.JavaCast() is { } javaBreadcrumb) + { + breadcrumbs.Add(javaBreadcrumb.ToBreadcrumb()); + } } + return breadcrumbs; + } + catch (Java.Lang.Exception) + { + return null; } - return breadcrumbs; } } From e0a2b3e3c2d7b1a59f752d855366ef314a2a880b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 4 Mar 2026 14:28:27 +0100 Subject: [PATCH 5/5] fix: Read persisted breadcrumbs inside options callback to avoid race condition Move breadcrumb reading into the OptionsConfigurationCallback, before SentryAndroid.Init() completes and integrations start writing to the QueueFile. Create a temporary PersistingScopeObserver to read from the previous session's data without concurrent modification. Co-Authored-By: Claude Opus 4.6 --- src/Sentry/Platforms/Android/SentrySdk.cs | 50 +++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 110381a2fa..314b04c1b9 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -173,6 +173,11 @@ private static void InitSentryAndroidSdk(SentryOptions options) o.ConnectionStatusProvider = new AndroidConnectionStatusProvider(AppContext, o, buildInfoProvider, timeProvider, mainHandler).JavaCast(); o.AddIntegration(new SystemEventsBreadcrumbsIntegration(AppContext, mainHandler).JavaCast()); + + // Read persisted breadcrumbs now, inside the options callback, before the SDK starts. + // This avoids a race condition with breadcrumb-producing integrations that start writing + // to the QueueFile as soon as SentryAndroid.Init() completes. + _persistedBreadcrumbs = ReadPersistedBreadcrumbs(o); }); // Now initialize the Android SDK (with a logger only if we're debugging) @@ -186,9 +191,6 @@ private static void InitSentryAndroidSdk(SentryOptions options) SentryAndroid.Init(AppContext, configuration); } - // Read persisted breadcrumbs from the previous session before they get overwritten - _persistedBreadcrumbs = ReadPersistedBreadcrumbs(nativeOptions!); - // Set options for the managed SDK that depend on the Android SDK. (The user will not be able to modify these.) options.AddEventProcessor(new AndroidEventProcessor(nativeOptions!)); if (options.Android.LogCatIntegration != LogCatIntegrationType.None) @@ -302,35 +304,31 @@ public static IReadOnlyList GetPersistedBreadcrumbs() => private static List? ReadPersistedBreadcrumbs(JavaSdk.SentryOptions nativeOptions) { - try + if (nativeOptions.CacheDirPath is null) { - var observer = nativeOptions.FindPersistingScopeObserver(); - if (observer is null) - { - return null; - } + return null; + } - var result = observer.Read(nativeOptions, "breadcrumbs.json", - Java.Lang.Class.FromType(typeof(Java.Util.IList))); + // Create a temporary observer to read breadcrumbs from the previous session's QueueFile. + // This must be called before SentryAndroid.Init() completes, to avoid a race condition + // with breadcrumb-producing integrations that write to the same QueueFile concurrently. + using var observer = new JavaSdk.Cache.PersistingScopeObserver(nativeOptions); + var result = observer.Read(nativeOptions, "breadcrumbs.json", + Java.Lang.Class.FromType(typeof(Java.Util.IList))); - if (result is not Java.Util.IList javaList) - { - return null; - } + if (result is not Java.Util.IList javaList) + { + return null; + } - var breadcrumbs = new List(); - for (var i = 0; i < javaList.Size(); i++) + var breadcrumbs = new List(); + for (var i = 0; i < javaList.Size(); i++) + { + if (javaList.Get(i)?.JavaCast() is { } javaBreadcrumb) { - if (javaList.Get(i)?.JavaCast() is { } javaBreadcrumb) - { - breadcrumbs.Add(javaBreadcrumb.ToBreadcrumb()); - } + breadcrumbs.Add(javaBreadcrumb.ToBreadcrumb()); } - return breadcrumbs; - } - catch (Java.Lang.Exception) - { - return null; } + return breadcrumbs; } }