diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a01c89915..d6ea619043 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 in MAUI apps on Android ([#4688](https://github.com/getsentry/sentry-dotnet/pull/4688)) ### Fixes diff --git a/integration-test/android.Tests.ps1 b/integration-test/android.Tests.ps1 index f7910a5af3..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) } @@ -201,4 +207,38 @@ 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 '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 "Managed" -TestCondition "OnSleep" { + xharness android adb -- shell input keyevent KEYCODE_HOME + } + } + + Dump-ServerErrors -Result $result + $result.HasErrors() | Should -BeFalse + @('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 c187a5ad66..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)) { diff --git a/integration-test/net9-maui/Platforms/Android/MainActivity.cs b/integration-test/net9-maui/Platforms/Android/MainActivity.cs index b59e6b60ec..635a980a70 100644 --- a/integration-test/net9-maui/Platforms/Android/MainActivity.cs +++ b/integration-test/net9-maui/Platforms/Android/MainActivity.cs @@ -20,5 +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")); + System.Environment.SetEnvironmentVariable("SENTRY_TEST_CONDITION", Intent?.GetStringExtra("SENTRY_TEST_CONDITION")); } } 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 =>