Skip to content
Merged
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 @@ -6,6 +6,8 @@

- Memory leak when finishing an unsampled Transaction that has started unsampled Spans ([#4717](https://github.com/getsentry/sentry-dotnet/pull/4717))
- backported via ([#4853](https://github.com/getsentry/sentry-dotnet/pull/4853))
- Deliver system breadcrumbs in the main thread on Android ([#4671](https://github.com/getsentry/sentry-dotnet/pull/4671))
- backported via ([#4856](https://github.com/getsentry/sentry-dotnet/pull/4856))

### Dependencies

Expand Down
24 changes: 24 additions & 0 deletions integration-test/android.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,28 @@ Describe 'MAUI app (<tfm>, <configuration>)' -ForEach @(
$result.Envelopes() | Should -HaveCount 1
}
}

It 'Delivers battery breadcrumbs in main thread (<configuration>)' {
$result = Invoke-SentryServer {
param([string]$url)
RunAndroidApp -Dsn $url -TestArg "BATTERY_CHANGED"
}

Dump-ServerErrors -Result $result
$result.HasErrors() | Should -BeFalse
$result.Envelopes() | Should -AnyElementMatch "`"type`":`"system`",`"thread_id`":`"1`",`"category`":`"device.event`",`"action`":`"BATTERY_CHANGED`""
$result.Envelopes() | Should -HaveCount 1
}

It 'Delivers network breadcrumbs in main thread (<configuration>)' {
$result = Invoke-SentryServer {
param([string]$url)
RunAndroidApp -Dsn $url -TestArg "NETWORK_CAPABILITIES_CHANGED"
}

Dump-ServerErrors -Result $result
$result.HasErrors() | Should -BeFalse
$result.Envelopes() | Should -AnyElementMatch "`"type`":`"system`",`"thread_id`":`"1`",`"category`":`"network.event`",`"action`":`"NETWORK_CAPABILITIES_CHANGED`""
$result.Envelopes() | Should -HaveCount 1
}
}
100 changes: 99 additions & 1 deletion integration-test/net9-maui/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,112 @@
namespace Sentry.Maui.Device.IntegrationTestApp;
#if ANDROID
using Android.OS;
#endif
using System.Collections.Concurrent;

namespace Sentry.Maui.Device.IntegrationTestApp;

public partial class App : Application
{
private static readonly ConcurrentDictionary<string, Dictionary<string, string>> systemBreadcrumbs = new();
private static string? testArg;

public App()
{
InitializeComponent();
}

public static bool HasTestArg(string arg)
{
return string.Equals(testArg, arg, StringComparison.OrdinalIgnoreCase);
}

public static void ReceiveSystemBreadcrumb(Breadcrumb breadcrumb)
{
if (breadcrumb.Type != "system" ||
breadcrumb.Data?.TryGetValue("action", out var action) != true ||
string.IsNullOrEmpty(action))
{
return;
}

systemBreadcrumbs[action] = new Dictionary<string, string>()
{
["action"] = action,
["category"] = breadcrumb.Category ?? string.Empty,
["thread_id"] = Thread.CurrentThread.ManagedThreadId.ToString(),
["type"] = breadcrumb.Type ?? string.Empty,
};

if (HasTestArg(action))
{
// received after OnAppearing
CaptureSystemBreadcrumb(action, systemBreadcrumbs[action]!);
Kill();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Race condition in breadcrumb test logic

Medium Severity

The test code has a race condition between ReceiveSystemBreadcrumb and OnAppearing. If a breadcrumb arrives after testArg is set (line 66) but before the breadcrumb check (line 88), both code paths execute. This calls CaptureSystemBreadcrumb twice and Kill twice, potentially sending duplicate events and causing the test to fail its envelope count assertion.

Additional Locations (1)

Fix in Cursor Fix in Web

}

public static void CaptureSystemBreadcrumb(string action, Dictionary<string, string> data)
{
SentrySdk.CaptureMessage(action, scope =>
{
foreach (var kvp in data)
{
scope.SetExtra(kvp.Key, kvp.Value);
}
});
}

protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}

public static void OnAppearing()
{
testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG");

#pragma warning disable CS0618
if (Enum.TryParse<CrashType>(testArg, ignoreCase: true, out var crashType))
{
SentrySdk.CauseCrash(crashType);
}
#pragma warning restore CS0618

if (HasTestArg("NullReferenceException"))
{
try
{
object? obj = null;
_ = obj!.ToString();
}
catch (NullReferenceException ex)
{
SentrySdk.CaptureException(ex);
}
Kill();
}
else if (!string.IsNullOrEmpty(testArg) && systemBreadcrumbs.TryGetValue(testArg, out var breadcrumb))
{
// received before OnAppearing
CaptureSystemBreadcrumb(testArg, breadcrumb);
Kill();
}
else if (HasTestArg("None"))
{
Kill();
}
}

public static void Kill()
{
SentrySdk.Close();

#if ANDROID
// prevent auto-restart
Platform.CurrentActivity?.FinishAffinity();
Process.KillProcess(Process.MyPid());
#elif IOS
System.Environment.Exit(0);
#endif
}
}
37 changes: 2 additions & 35 deletions integration-test/net9-maui/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
#if ANDROID
using Android.OS;
#endif

namespace Sentry.Maui.Device.IntegrationTestApp;
namespace Sentry.Maui.Device.IntegrationTestApp;

public partial class MainPage : ContentPage
{
Expand All @@ -15,35 +11,6 @@ protected override void OnAppearing()
{
base.OnAppearing();

var testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG");

#pragma warning disable CS0618
if (Enum.TryParse<CrashType>(testArg, ignoreCase: true, out var crashType))
{
SentrySdk.CauseCrash(crashType);
}
#pragma warning restore CS0618

if (testArg?.Equals("NullReferenceException", StringComparison.OrdinalIgnoreCase) == true)
{
try
{
object? obj = null;
_ = obj!.ToString();
}
catch (NullReferenceException ex)
{
SentrySdk.CaptureException(ex);
}
}

SentrySdk.Close();
#if ANDROID
// prevent auto-restart
Platform.CurrentActivity?.FinishAffinity();
Process.KillProcess(Process.MyPid());
#elif IOS
System.Environment.Exit(0);
#endif
App.OnAppearing();
}
}
6 changes: 6 additions & 0 deletions integration-test/net9-maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public static MauiApp CreateMauiApp()
// predictable crash envelopes only
options.SendClientReports = false;
options.AutoSessionTracking = false;

options.SetBeforeBreadcrumb((breadcrumb, hint) =>
{
App.ReceiveSystemBreadcrumb(breadcrumb);
return breadcrumb;
});
})
.ConfigureFonts(fonts =>
{
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Bindings.Android/Transforms/Metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
</method>
</add-node>

<!-- Remove problematic deprecated methods in the IScopes interface -->
<remove-node path="/api/package[@name='io.sentry']/interface[@name='IScopes']/method[@deprecated]" />

<!--
TODO: If we need this, figure out how to multi-target or late bind.
This API uses FrameMetrics, which requires Android >= 24.0. We currently target Android >= 21.0 which is the minimum supported by MAUI.
Expand Down
12 changes: 6 additions & 6 deletions src/Sentry/Platforms/Android/AndroidDiagnosticLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ namespace Sentry.Android;

internal class AndroidDiagnosticLogger : JavaObject, JavaSdk.ILogger
{
private readonly IDiagnosticLogger _logger;
private readonly IDiagnosticLogger? _logger;

public AndroidDiagnosticLogger(IDiagnosticLogger logger) => _logger = logger;
public AndroidDiagnosticLogger(IDiagnosticLogger? logger) => _logger = logger;

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

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

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

public bool IsEnabled(JavaSdk.SentryLevel? level) =>
level != null && _logger.IsEnabled(level.ToSentryLevel());
level != null && _logger != null && _logger.IsEnabled(level.ToSentryLevel());

private static string FormatJavaString(string s, JavaObject[]? args) =>
args is null ? s : JavaString.Format(s, args);
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry/Platforms/Android/Sentry.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<Using Include="Android.Runtime" />
<Using Include="Android.Content.Context" Alias="AndroidContext" />
<Using Include="Android.OS.Build" Alias="AndroidBuild" />
<Using Include="Android.OS.Looper" Alias="AndroidLooper" />
<Using Include="Android.OS.Handler" Alias="AndroidHandler" />
<Using Include="Java.Lang.Boolean" Alias="JavaBoolean" />
<Using Include="Java.Lang.Class" Alias="JavaClass" />
<Using Include="Java.Lang.Double" Alias="JavaDouble" />
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Platforms/Android/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using Sentry.Android.Callbacks;
using Sentry.Android.Extensions;
using Sentry.Extensibility;
using Sentry.JavaSdk;
using Sentry.JavaSdk.Android.Core;
using Sentry.JavaSdk.Android.Core.Internal.Util;

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

// Don't capture managed exceptions in the native SDK, since we already capture them in the managed SDK
o.AddIgnoredExceptionForType(JavaClass.ForName("android.runtime.JavaProxyThrowable"));

// Deliver network and system event breadcrumbs in the main thread
// See https://github.com/getsentry/sentry-dotnet/issues/3828
var networkLogger = new AndroidDiagnosticLogger(options.DiagnosticLogger);
var buildInfoProvider = new BuildInfoProvider(networkLogger);
var timeProvider = AndroidCurrentDateProvider.Instance!;
var mainHandler = new AndroidHandler(AndroidLooper.MainLooper!);
o.ConnectionStatusProvider =
new AndroidConnectionStatusProvider(AppContext, o, buildInfoProvider, timeProvider, mainHandler).JavaCast<IConnectionStatusProvider>();
o.AddIntegration(new SystemEventsBreadcrumbsIntegration(AppContext, mainHandler).JavaCast<JavaSdk.IIntegration>());
});

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