From 2b4a366db0861adb723673db42d49b2f5dcb4813 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 3 Nov 2025 15:50:41 +0100 Subject: [PATCH 1/5] feat(maui): add missing Activity lifecycle events on Android --- .../AndroidActivityBreadcrumbsIntegration.cs | 39 +++++++++++++++++++ .../Internal/SentryMauiOptionsSetup.cs | 5 +++ .../SentryMauiAppBuilderExtensions.cs | 2 + 3 files changed, 46 insertions(+) create mode 100644 src/Sentry.Maui/Internal/AndroidActivityBreadcrumbsIntegration.cs diff --git a/src/Sentry.Maui/Internal/AndroidActivityBreadcrumbsIntegration.cs b/src/Sentry.Maui/Internal/AndroidActivityBreadcrumbsIntegration.cs new file mode 100644 index 0000000000..1405ba94a9 --- /dev/null +++ b/src/Sentry.Maui/Internal/AndroidActivityBreadcrumbsIntegration.cs @@ -0,0 +1,39 @@ +#if ANDROID +using Microsoft.Maui.LifecycleEvents; +using Activity = Android.App.Activity; + +namespace Sentry.Maui.Internal; + +// Capture Android Activity lifecycle events as breadcrumbs. +// See: https://github.com/getsentry/sentry-java/blob/ab8a72db41b2e5c66e60cef3102294dddba90b20/sentry-android-core/src/main/java/io/sentry/android/core/ActivityBreadcrumbsIntegration.java +internal static class AndroidActivityBreadcrumbsIntegration +{ + public static void Register(IAndroidLifecycleBuilder lifecycle) + { + lifecycle.OnCreate((activity, _) => AddBreadcrumb(activity, "created")); + lifecycle.OnStart(activity => AddBreadcrumb(activity, "started")); + lifecycle.OnResume(activity => AddBreadcrumb(activity, "resumed")); + lifecycle.OnPause(activity => AddBreadcrumb(activity, "paused")); + lifecycle.OnStop(activity => AddBreadcrumb(activity, "stopped")); + lifecycle.OnSaveInstanceState((activity, _) => AddBreadcrumb(activity, "saveInstanceState")); + lifecycle.OnDestroy(activity => AddBreadcrumb(activity, "destroyed")); + } + + private static void AddBreadcrumb(Activity activity, string state) + { + var breadcrumb = new Breadcrumb( + DateTimeOffset.UtcNow, + message: null, + type: MauiEventsBinder.NavigationType, + data: new Dictionary + { + { "screen", activity.Class.SimpleName }, + { "state", state } + }, + category: MauiEventsBinder.LifecycleCategory, + level: BreadcrumbLevel.Info + ); + SentrySdk.AddBreadcrumb(breadcrumb); + } +} +#endif diff --git a/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs b/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs index 7436d0720e..e2c87d9097 100644 --- a/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs +++ b/src/Sentry.Maui/Internal/SentryMauiOptionsSetup.cs @@ -26,6 +26,11 @@ public void Configure(SentryMauiOptions options) _config.Bind(bindable); bindable.ApplyTo(options); +#if __ANDROID__ + // Disable Android Activity lifecycle breadcrumbs as Sentry.Maui already tracks these. + options.Native.EnableActivityLifecycleBreadcrumbs = false; +#endif + #if __ANDROID__ || __IOS__ options.Native.AttachScreenshot = options.AttachScreenshot; #endif diff --git a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs index 9274cf5812..f9e26307d6 100644 --- a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs +++ b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs @@ -140,6 +140,8 @@ private static void RegisterMauiEventsBinder(this MauiAppBuilder builder) lifecycle.OnStop(activity => SentryMauiEventProcessor.InForeground = false); lifecycle.OnPause(activity => SentryMauiEventProcessor.InForeground = false); + + AndroidActivityBreadcrumbsIntegration.Register(lifecycle); }); #elif WINDOWS events.AddWindows(lifecycle => From 92dd76b2bbd704e4942c9ba9e8ba31c1746bdcd1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Nov 2025 14:04:35 +0100 Subject: [PATCH 2/5] Add integration test --- integration-test/android.Tests.ps1 | 14 ++++++++++++++ integration-test/net9-maui/App.xaml.cs | 6 ++++++ .../net9-maui/Platforms/Android/MainActivity.cs | 11 +++++++++++ 3 files changed, 31 insertions(+) diff --git a/integration-test/android.Tests.ps1 b/integration-test/android.Tests.ps1 index f7910a5af3..94cded5bb3 100644 --- a/integration-test/android.Tests.ps1 +++ b/integration-test/android.Tests.ps1 @@ -201,4 +201,18 @@ Describe 'MAUI app (, )' -ForEach @( $result.Envelopes() | Should -AnyElementMatch "`"type`":`"system`",`"thread_id`":`"1`",`"category`":`"network.event`",`"action`":`"NETWORK_CAPABILITIES_CHANGED`"" $result.Envelopes() | Should -HaveCount 1 } + + It 'Lifecycle events ()' { + $result = Invoke-SentryServer { + param([string]$url) + RunAndroidApp -Dsn $url -TestArg "Background" + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + @('created', 'started', 'resumed', 'paused', 'stopped') | ForEach-Object { + $result.Envelopes() | Should -AnyElementMatch "`"type`":`"navigation`",`"data`":{`"screen`":`"MainActivity`",`"state`":`"$_`"},`"category`":`"ui.lifecycle`"" + } + $result.Envelopes() | Should -HaveCount 1 + } } diff --git a/integration-test/net9-maui/App.xaml.cs b/integration-test/net9-maui/App.xaml.cs index c187a5ad66..ead0e761e1 100644 --- a/integration-test/net9-maui/App.xaml.cs +++ b/integration-test/net9-maui/App.xaml.cs @@ -91,6 +91,12 @@ public static void OnAppearing() CaptureSystemBreadcrumb(testArg, breadcrumb); Kill(); } + else if (HasTestArg("Background")) + { +#if ANDROID + Platform.CurrentActivity?.MoveTaskToBack(true); +#endif + } else if (HasTestArg("None")) { Kill(); diff --git a/integration-test/net9-maui/Platforms/Android/MainActivity.cs b/integration-test/net9-maui/Platforms/Android/MainActivity.cs index b59e6b60ec..d15701be5f 100644 --- a/integration-test/net9-maui/Platforms/Android/MainActivity.cs +++ b/integration-test/net9-maui/Platforms/Android/MainActivity.cs @@ -21,4 +21,15 @@ protected override void OnCreate(Bundle? savedInstanceState) System.Environment.SetEnvironmentVariable("SENTRY_DSN", Intent?.GetStringExtra("SENTRY_DSN")); System.Environment.SetEnvironmentVariable("SENTRY_TEST_ARG", Intent?.GetStringExtra("SENTRY_TEST_ARG")); } + + protected override void OnStop() + { + base.OnStop(); + + if (App.HasTestArg("Background")) + { + SentrySdk.CaptureMessage("Background"); + App.Kill(); + } + } } From d536341a471be9d14b5982c4576dd095bf929794 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Nov 2025 14:07:19 +0100 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a01c89915..9ccdab4419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633), [#4653](https://github.com/getsentry/sentry-dotnet/pull/4653)) - The SDK now provides a `IsSessionActive` to allow checking the session state ([#4662](https://github.com/getsentry/sentry-dotnet/pull/4662)) - The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633)) +- Add missing Activity lifecycle events on Android ([#4688](https://github.com/getsentry/sentry-dotnet/pull/4688)) ### Fixes From d6fe871d28e211617be4349d0a89bc25268067db Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Nov 2025 10:03:33 +0100 Subject: [PATCH 4/5] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ccdab4419..d6ea619043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633), [#4653](https://github.com/getsentry/sentry-dotnet/pull/4653)) - The SDK now provides a `IsSessionActive` to allow checking the session state ([#4662](https://github.com/getsentry/sentry-dotnet/pull/4662)) - The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633)) -- Add missing Activity lifecycle events on Android ([#4688](https://github.com/getsentry/sentry-dotnet/pull/4688)) +- Add missing Activity lifecycle events in MAUI apps on Android ([#4688](https://github.com/getsentry/sentry-dotnet/pull/4688)) ### Fixes From 0f9cdfc9ea82884bb82fcdb0cef6a54ec0543cca Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Nov 2025 13:20:31 +0100 Subject: [PATCH 5/5] Revise integration test --- integration-test/android.Tests.ps1 | 40 +++++++++++++++---- integration-test/net9-maui/App.xaml.cs | 31 +++++++++++--- .../Platforms/Android/MainActivity.cs | 12 +----- 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/integration-test/android.Tests.ps1 b/integration-test/android.Tests.ps1 index 94cded5bb3..42ad493ac0 100644 --- a/integration-test/android.Tests.ps1 +++ b/integration-test/android.Tests.ps1 @@ -54,14 +54,17 @@ Describe 'MAUI app (, )' -ForEach @( { param( [string] $Dsn, - [string] $TestArg = 'None' + [string] $TestArg = 'None', + [string] $TestCondition = 'OnAppearing', + [scriptblock] $Callback = $null ) Write-Host "::group::Run Android app (TestArg=$TestArg)" $dsn = $Dsn.Replace('http://', 'http://key@') + '/0' xharness android adb -v ` -- shell am start -S -n io.sentry.dotnet.maui.device.integrationtestapp/.MainActivity ` -e SENTRY_DSN $dsn ` - -e SENTRY_TEST_ARG $TestArg + -e SENTRY_TEST_ARG $TestArg ` + -e SENTRY_TEST_CONDITION $TestCondition | ForEach-Object { Write-Host $_ } Write-Host '::endgroup::' $LASTEXITCODE | Should -Be 0 @@ -73,7 +76,10 @@ Describe 'MAUI app (, )' -ForEach @( $procid = (& xharness android adb -- shell pidof "io.sentry.dotnet.maui.device.integrationtestapp") -replace '\s', '' $activity = (& xharness android adb -- shell dumpsys activity activities) -match "io\.sentry\.dotnet\.maui\.device\.integrationtestapp" - + if ($procid -and $activity -and $Callback) + { + & $Callback + } } while ($procid -and $activity) } @@ -202,16 +208,36 @@ Describe 'MAUI app (, )' -ForEach @( $result.Envelopes() | Should -HaveCount 1 } - It 'Lifecycle events ()' { + It 'Native native lifecycle events' { + $result = Invoke-SentryServer { + param([string]$url) + RunAndroidApp -Dsn $url -TestArg "Native" -TestCondition "OnSleep" { + xharness android adb -- shell input keyevent KEYCODE_HOME + } + RunAndroidApp -Dsn $url + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + @('created', 'started', 'resumed', 'paused') | ForEach-Object { + # TODO: why is native breadcrumb data a string instead of object? + $result.Envelopes() | Should -AnyElementMatch ('"type":"navigation","data":"{\"screen\":\"MainActivity\",\"state\":\"' + $_ + '\"}\","category":"ui.lifecycle"') + } + $result.Envelopes() | Should -HaveCount 1 + } + + It 'Managed lifecycle events' { $result = Invoke-SentryServer { param([string]$url) - RunAndroidApp -Dsn $url -TestArg "Background" + RunAndroidApp -Dsn $url -TestArg "Managed" -TestCondition "OnSleep" { + xharness android adb -- shell input keyevent KEYCODE_HOME + } } Dump-ServerErrors -Result $result $result.HasErrors() | Should -BeFalse - @('created', 'started', 'resumed', 'paused', 'stopped') | ForEach-Object { - $result.Envelopes() | Should -AnyElementMatch "`"type`":`"navigation`",`"data`":{`"screen`":`"MainActivity`",`"state`":`"$_`"},`"category`":`"ui.lifecycle`"" + @('created', 'started', 'resumed', 'paused') | ForEach-Object { + $result.Envelopes() | Should -AnyElementMatch ('"type":"navigation","data":{"screen":"MainActivity","state":"' + $_ + '"},"category":"ui.lifecycle"') } $result.Envelopes() | Should -HaveCount 1 } diff --git a/integration-test/net9-maui/App.xaml.cs b/integration-test/net9-maui/App.xaml.cs index ead0e761e1..b391c6cfa9 100644 --- a/integration-test/net9-maui/App.xaml.cs +++ b/integration-test/net9-maui/App.xaml.cs @@ -9,6 +9,7 @@ public partial class App : Application { private static readonly ConcurrentDictionary> systemBreadcrumbs = new(); private static string? testArg; + private static string? testCondition; public App() { @@ -20,6 +21,11 @@ public static bool HasTestArg(string arg) return string.Equals(testArg, arg, StringComparison.OrdinalIgnoreCase); } + public static bool HasTestCondition(string condition) + { + return string.Equals(testCondition, condition, StringComparison.OrdinalIgnoreCase); + } + public static void ReceiveSystemBreadcrumb(Breadcrumb breadcrumb) { if (breadcrumb.Type != "system" || @@ -64,7 +70,26 @@ protected override Window CreateWindow(IActivationState? activationState) public static void OnAppearing() { testArg = System.Environment.GetEnvironmentVariable("SENTRY_TEST_ARG"); + testCondition = System.Environment.GetEnvironmentVariable("SENTRY_TEST_CONDITION"); + + if (HasTestCondition("OnAppearing")) + { + RunTest(); + } + } + + protected override void OnSleep() + { + base.OnSleep(); + if (HasTestCondition("OnSleep")) + { + RunTest(); + } + } + + private static void RunTest() + { #pragma warning disable CS0618 if (Enum.TryParse(testArg, ignoreCase: true, out var crashType)) { @@ -91,12 +116,6 @@ public static void OnAppearing() CaptureSystemBreadcrumb(testArg, breadcrumb); Kill(); } - else if (HasTestArg("Background")) - { -#if ANDROID - Platform.CurrentActivity?.MoveTaskToBack(true); -#endif - } else if (HasTestArg("None")) { Kill(); diff --git a/integration-test/net9-maui/Platforms/Android/MainActivity.cs b/integration-test/net9-maui/Platforms/Android/MainActivity.cs index d15701be5f..635a980a70 100644 --- a/integration-test/net9-maui/Platforms/Android/MainActivity.cs +++ b/integration-test/net9-maui/Platforms/Android/MainActivity.cs @@ -20,16 +20,6 @@ protected override void OnCreate(Bundle? savedInstanceState) System.Environment.SetEnvironmentVariable("SENTRY_DSN", Intent?.GetStringExtra("SENTRY_DSN")); System.Environment.SetEnvironmentVariable("SENTRY_TEST_ARG", Intent?.GetStringExtra("SENTRY_TEST_ARG")); - } - - protected override void OnStop() - { - base.OnStop(); - - if (App.HasTestArg("Background")) - { - SentrySdk.CaptureMessage("Background"); - App.Kill(); - } + System.Environment.SetEnvironmentVariable("SENTRY_TEST_CONDITION", Intent?.GetStringExtra("SENTRY_TEST_CONDITION")); } }