Skip to content

Commit cec5cb7

Browse files
committed
fix: deliver system breadcrumbs in the main thread on Android
1 parent d95347e commit cec5cb7

File tree

9 files changed

+155
-43
lines changed

9 files changed

+155
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
### Fixes
1818

1919
- The `Serilog` integration captures _Structured Logs_ (when enabled) independently of captured Events and added Breadcrumbs ([#4691](https://github.com/getsentry/sentry-dotnet/pull/4691))
20+
- Deliver system breadcrumbs in the main thread on Android ([#4671](https://github.com/getsentry/sentry-dotnet/pull/4671))
2021

2122
## 6.0.0-preview.2
2223

integration-test/android.Tests.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,28 @@ Describe 'MAUI app (<tfm>, <configuration>)' -ForEach @(
177177
$result.Envelopes() | Should -HaveCount 1
178178
}
179179
}
180+
181+
It 'Delivers battery breadcrumbs in main thread (<configuration>)' {
182+
$result = Invoke-SentryServer {
183+
param([string]$url)
184+
RunAndroidApp -Dsn $url -TestArg "BATTERY_CHANGED"
185+
}
186+
187+
Dump-ServerErrors -Result $result
188+
$result.HasErrors() | Should -BeFalse
189+
$result.Envelopes() | Should -AnyElementMatch "`"type`":`"system`",`"thread_id`":`"1`",`"category`":`"device.event`",`"action`":`"BATTERY_CHANGED`""
190+
$result.Envelopes() | Should -HaveCount 1
191+
}
192+
193+
It 'Delivers network breadcrumbs in main thread (<configuration>)' {
194+
$result = Invoke-SentryServer {
195+
param([string]$url)
196+
RunAndroidApp -Dsn $url -TestArg "NETWORK_CAPABILITIES_CHANGED"
197+
}
198+
199+
Dump-ServerErrors -Result $result
200+
$result.HasErrors() | Should -BeFalse
201+
$result.Envelopes() | Should -AnyElementMatch "`"type`":`"system`",`"thread_id`":`"1`",`"category`":`"network.event`",`"action`":`"NETWORK_CAPABILITIES_CHANGED`""
202+
$result.Envelopes() | Should -HaveCount 1
203+
}
180204
}
Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,112 @@
1-
namespace Sentry.Maui.Device.IntegrationTestApp;
1+
#if ANDROID
2+
using Android.OS;
3+
#endif
4+
using System.Collections.Concurrent;
5+
6+
namespace Sentry.Maui.Device.IntegrationTestApp;
27

38
public partial class App : Application
49
{
10+
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> systemBreadcrumbs = new();
11+
private static string? testArg;
12+
513
public App()
614
{
715
InitializeComponent();
816
}
917

18+
private static bool HasTestArg(string arg)
19+
{
20+
return string.Equals(testArg, arg, StringComparison.OrdinalIgnoreCase);
21+
}
22+
23+
public static void ReceiveSystemBreadcrumb(Breadcrumb breadcrumb)
24+
{
25+
if (breadcrumb.Type != "system" ||
26+
breadcrumb.Data?.TryGetValue("action", out var action) != true ||
27+
string.IsNullOrEmpty(action))
28+
{
29+
return;
30+
}
31+
32+
systemBreadcrumbs[action] = new Dictionary<string, string>()
33+
{
34+
["action"] = action,
35+
["category"] = breadcrumb.Category ?? string.Empty,
36+
["thread_id"] = Thread.CurrentThread.ManagedThreadId.ToString(),
37+
["type"] = breadcrumb.Type ?? string.Empty,
38+
};
39+
40+
if (HasTestArg(action))
41+
{
42+
// received after OnAppearing
43+
CaptureSystemBreadcrumb(action, systemBreadcrumbs[action]!);
44+
Kill();
45+
}
46+
}
47+
48+
public static void CaptureSystemBreadcrumb(string action, Dictionary<string, string> data)
49+
{
50+
SentrySdk.CaptureMessage(action, scope =>
51+
{
52+
foreach (var kvp in data)
53+
{
54+
scope.SetExtra(kvp.Key, kvp.Value);
55+
}
56+
});
57+
}
58+
1059
protected override Window CreateWindow(IActivationState? activationState)
1160
{
1261
return new Window(new AppShell());
1362
}
63+
64+
public static void OnAppearing()
65+
{
66+
testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG");
67+
68+
#pragma warning disable CS0618
69+
if (Enum.TryParse<CrashType>(testArg, ignoreCase: true, out var crashType))
70+
{
71+
SentrySdk.CauseCrash(crashType);
72+
}
73+
#pragma warning restore CS0618
74+
75+
if (HasTestArg("NullReferenceException"))
76+
{
77+
try
78+
{
79+
object? obj = null;
80+
_ = obj!.ToString();
81+
}
82+
catch (NullReferenceException ex)
83+
{
84+
SentrySdk.CaptureException(ex);
85+
}
86+
Kill();
87+
}
88+
else if (!string.IsNullOrEmpty(testArg) && systemBreadcrumbs.TryGetValue(testArg, out var breadcrumb))
89+
{
90+
// received before OnAppearing
91+
CaptureSystemBreadcrumb(testArg, breadcrumb);
92+
Kill();
93+
}
94+
else if (HasTestArg("None"))
95+
{
96+
Kill();
97+
}
98+
}
99+
100+
public static void Kill()
101+
{
102+
SentrySdk.Close();
103+
104+
#if ANDROID
105+
// prevent auto-restart
106+
Platform.CurrentActivity?.FinishAffinity();
107+
Process.KillProcess(Process.MyPid());
108+
#elif IOS
109+
System.Environment.Exit(0);
110+
#endif
111+
}
14112
}
Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
#if ANDROID
2-
using Android.OS;
3-
#endif
4-
5-
namespace Sentry.Maui.Device.IntegrationTestApp;
1+
namespace Sentry.Maui.Device.IntegrationTestApp;
62

73
public partial class MainPage : ContentPage
84
{
@@ -14,36 +10,6 @@ public MainPage()
1410
protected override void OnAppearing()
1511
{
1612
base.OnAppearing();
17-
18-
var testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG");
19-
20-
#pragma warning disable CS0618
21-
if (Enum.TryParse<CrashType>(testArg, ignoreCase: true, out var crashType))
22-
{
23-
SentrySdk.CauseCrash(crashType);
24-
}
25-
#pragma warning restore CS0618
26-
27-
if (testArg?.Equals("NullReferenceException", StringComparison.OrdinalIgnoreCase) == true)
28-
{
29-
try
30-
{
31-
object? obj = null;
32-
_ = obj!.ToString();
33-
}
34-
catch (NullReferenceException ex)
35-
{
36-
SentrySdk.CaptureException(ex);
37-
}
38-
}
39-
40-
SentrySdk.Close();
41-
#if ANDROID
42-
// prevent auto-restart
43-
Platform.CurrentActivity?.FinishAffinity();
44-
Process.KillProcess(Process.MyPid());
45-
#elif IOS
46-
System.Environment.Exit(0);
47-
#endif
13+
App.OnAppearing();
4814
}
4915
}

integration-test/net9-maui/MauiProgram.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ public static MauiApp CreateMauiApp()
2020
// predictable crash envelopes only
2121
options.SendClientReports = false;
2222
options.AutoSessionTracking = false;
23+
24+
options.SetBeforeBreadcrumb((breadcrumb, hint) =>
25+
{
26+
App.ReceiveSystemBreadcrumb(breadcrumb);
27+
return breadcrumb;
28+
});
2329
})
2430
.ConfigureFonts(fonts =>
2531
{

src/Sentry.Bindings.Android/Transforms/Metadata.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@
8585
</method>
8686
</add-node>
8787

88+
<!-- Remove problematic deprecated methods in the IScopes interface -->
89+
<remove-node path="/api/package[@name='io.sentry']/interface[@name='IScopes']/method[@deprecated]" />
90+
8891
<!--
8992
TODO: If we need this, figure out how to multi-target or late bind.
9093
This API uses FrameMetrics, which requires Android >= 24.0. We currently target Android >= 21.0 which is the minimum supported by MAUI.

src/Sentry/Platforms/Android/AndroidDiagnosticLogger.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ namespace Sentry.Android;
55

66
internal class AndroidDiagnosticLogger : JavaObject, JavaSdk.ILogger
77
{
8-
private readonly IDiagnosticLogger _logger;
8+
private readonly IDiagnosticLogger? _logger;
99

10-
public AndroidDiagnosticLogger(IDiagnosticLogger logger) => _logger = logger;
10+
public AndroidDiagnosticLogger(IDiagnosticLogger? logger) => _logger = logger;
1111

1212
public void Log(JavaSdk.SentryLevel level, string message, JavaObject[]? args) =>
13-
_logger.Log(level.ToSentryLevel(), "Android: " + FormatJavaString(message, args));
13+
_logger?.Log(level.ToSentryLevel(), "Android: " + FormatJavaString(message, args));
1414

1515
public void Log(JavaSdk.SentryLevel level, string message, Throwable? throwable) =>
16-
_logger.Log(level.ToSentryLevel(), "Android: " + message, throwable);
16+
_logger?.Log(level.ToSentryLevel(), "Android: " + message, throwable);
1717

1818
public void Log(JavaSdk.SentryLevel level, Throwable? throwable, string message, params JavaObject[]? args) =>
19-
_logger.Log(level.ToSentryLevel(), "Android: " + FormatJavaString(message, args), throwable);
19+
_logger?.Log(level.ToSentryLevel(), "Android: " + FormatJavaString(message, args), throwable);
2020

2121
public bool IsEnabled(JavaSdk.SentryLevel? level) =>
22-
level != null && _logger.IsEnabled(level.ToSentryLevel());
22+
level != null && _logger != null && _logger.IsEnabled(level.ToSentryLevel());
2323

2424
private static string FormatJavaString(string s, JavaObject[]? args) =>
2525
args is null ? s : JavaString.Format(s, args);

src/Sentry/Platforms/Android/Sentry.Android.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
<Using Include="Android.Runtime" />
1010
<Using Include="Android.Content.Context" Alias="AndroidContext" />
1111
<Using Include="Android.OS.Build" Alias="AndroidBuild" />
12+
<Using Include="Android.OS.Looper" Alias="AndroidLooper" />
13+
<Using Include="Android.OS.Handler" Alias="AndroidHandler" />
1214
<Using Include="Java.Lang.Boolean" Alias="JavaBoolean" />
1315
<Using Include="Java.Lang.Class" Alias="JavaClass" />
1416
<Using Include="Java.Lang.Double" Alias="JavaDouble" />

src/Sentry/Platforms/Android/SentrySdk.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using Sentry.Android.Callbacks;
55
using Sentry.Android.Extensions;
66
using Sentry.Extensibility;
7+
using Sentry.JavaSdk;
78
using Sentry.JavaSdk.Android.Core;
9+
using Sentry.JavaSdk.Android.Core.Internal.Util;
810

911
// Don't let the Sentry Android SDK auto-init, as we do that manually in SentrySdk.Init
1012
// See https://docs.sentry.io/platforms/android/configuration/manual-init/
@@ -155,6 +157,16 @@ private static void InitSentryAndroidSdk(SentryOptions options)
155157

156158
// Don't capture managed exceptions in the native SDK, since we already capture them in the managed SDK
157159
o.AddIgnoredExceptionForType(JavaClass.ForName("android.runtime.JavaProxyThrowable"));
160+
161+
// Deliver network and system event breadcrumbs in the main thread
162+
// See https://github.com/getsentry/sentry-dotnet/issues/3828
163+
var networkLogger = new AndroidDiagnosticLogger(options.DiagnosticLogger);
164+
var buildInfoProvider = new BuildInfoProvider(networkLogger);
165+
var timeProvider = AndroidCurrentDateProvider.Instance!;
166+
var mainHandler = new AndroidHandler(AndroidLooper.MainLooper!);
167+
o.ConnectionStatusProvider =
168+
new AndroidConnectionStatusProvider(AppContext, o, buildInfoProvider, timeProvider, mainHandler).JavaCast<IConnectionStatusProvider>();
169+
o.AddIntegration(new SystemEventsBreadcrumbsIntegration(AppContext, mainHandler).JavaCast<JavaSdk.IIntegration>());
158170
});
159171

160172
// Now initialize the Android SDK (with a logger only if we're debugging)

0 commit comments

Comments
 (0)