DEV Community

Katherine Hambley
Katherine Hambley

Posted on

Local Crash Logging in .NET MAUI mobile apps

I know there are several options for logging crashes in .NET MAUI mobile apps. Google Firebase Crashlytics API being among one of the most popular platforms. However, I wanted to implement a really simple, down and dirty, crash logging system in my mobile apps, mainly while developing and debugging to help me discover issues quicker. This did the trick for me, maybe it will help you too!

The cleaniest most efficient way to log crashes locally. What this does:

  • [ ] Globally handle exceptions in the App.xaml.cs file.
  • [ ] Handle post-launch crashes.
  • [ ] Use SecureStorage to persist the log even after an app restart or rebuild
  • [ ] Email the log file as an attachment by clicking a button in the app.
  • [ ] Logs the last crash only

What this doesn't do:

  • [ ] Handle pre-launch or app-launch crashes.
  • [ ] Log multiple crashes and persist crash history

Basically, if the app crashes during launch (the launch screen appears), it won't send an email or at least you won't be able to get into the app to attach and email the log. You'd have to set up an API for that and have the crash log sent automatically to the cloud or wherever you host the log data. I'm working on a part two of this post showing how you can implement remote crash logging in .NET MAUI.

Step 1: I added the following to the App.xaml.cs file:

using Microsoft.Maui.Storage;
using System;
using System.Threading.Tasks;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        // Capture unhandled exceptions
        AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
        TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedException;
    }

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

    private async void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        await SaveCrashLogAsync(e.ExceptionObject as Exception);
    }

    private async void OnTaskSchedulerUnobservedException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        await SaveCrashLogAsync(e.Exception);
    }

    private async Task SaveCrashLogAsync(Exception ex)
    {
        if (ex == null) return;

        string crashMessage = $"[{DateTime.Now}] {ex}\n";

        try
        {

            if (DeviceInfo.Platform == DevicePlatform.Android)
            {
            // Use Preferences for Android (SharedPreferences equivalent)
            Preferences.Set("LastCrashLog", crashMessage);
            }
            else
            {
            // Use SecureStorage for iOS
            await SecureStorage.SetAsync("LastCrashLog", crashMessage);
            }
        }
        catch (Exception storageEx)
        {
        Console.WriteLine($"SecureStorage failed on Android: {storageEx.Message}"); 
        }   
    }
}

Enter fullscreen mode Exit fullscreen mode

So, you'll notice the different implementations for Android and iOS. I prefer to use SharedPreferences (Preferences API in .NET MAUI) on Android instead of SecureStorage. SecureStorage is unreliable on Android and there's a lot more setup involved. Since this is just a simple implementation and it only logs the last crash and sends it, we can use the Preferences API to make things simpler.

Now, for the $64 million decision, where do you put the test crash and send crash log buttons?

I typically put mine on the MenuPage. While you're developing, it doesn't matter where they are, so long as you can navigate to them quickly.

Menu Page with buttons screenshot

Step 2: In the MenuPage.xaml (or wherever you want the buttons), add the following:

<?xml version="1.0" encoding="utf-8" ?>

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            x:Class="EconomyNow2.Pages.MenuPage"
            Title="Menu">

    <VerticalStackLayout Padding="20">
        <Button Margin="0, 20, 0, 20"
                Text="Test Crash"
                Clicked="OnTestCrashClicked"
                HorizontalOptions="Fill" />

        <Button Text="Send Crash Log"
                Clicked="OnSendCrashLogClicked"
                HorizontalOptions="Fill" />
    </VerticalStackLayout>
</ContentPage>
Enter fullscreen mode Exit fullscreen mode

Step 3: In the MenuPage.xaml.cs (code-behind) file, add the following:

public partial class MenuPage : ContentPage
{
    public MenuPage()
    {
        InitializeComponent();
    }

    private async void OnSendCrashLogClicked(object sender, EventArgs e)
    {
        await EmailCrashLogAsync();
    }

    private void OnTestCrashClicked(object sender, EventArgs e)
    {
        throw new Exception("Test crash for logging!");
    }

    async Task EmailCrashLogAsync()
    {
        string crashLog = "";

        if(DeviceInfo.Platform == DevicePlatform.Android)
        {
            crashLog = Preferences.Get("LastCrashLog", "Unknown");
        }
        else
        {
            crashLog = await SecureStorage.GetAsync("LastCrashLog");
        }

        if (!string.IsNullOrEmpty(crashLog))
        {
            // Write crash log to a temporary file before sending
            string logFilePath = Path.Combine(FileSystem.CacheDirectory,
             $"crashlog_"+ DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + 
             ".txt");

            await File.WriteAllTextAsync(logFilePath, crashLog);

            var message = new EmailMessage
            {
                Subject = "App Crash Report - " + 
                DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
                Body = "Device Platform: " + DeviceInfo.Platform + "\n" +
                "OS Version: " + DeviceInfo.Current.VersionString + ".\n" +
                "Model: " + DeviceInfo.Current.Model + "\n" +
                "Manufacturer: " + DeviceInfo.Current.Manufacturer + "\n" +
                "Name: " + DeviceInfo.Current.Name + "\n" +
                "OS Version: " + DeviceInfo.Current.VersionString + "\n" +
                "Idiom: " + DeviceInfo.Current.Idiom + "\n" +
                "Platform: " + DeviceInfo.Current.Platform + "\n" +
                "Attached is the latest crash log.",
                To = new List<string> { "<INSERT-YOUR-EMAIL-HERE>" },
                Attachments = new List<EmailAttachment>
                {
                    new EmailAttachment(logFilePath)
                }
            };

            await MainThread.InvokeOnMainThreadAsync(async () =>
            {
                try
                {
                    await Email.Default.ComposeAsync(message);

                    // Delete log after sending
                    SecureStorage.Remove("LastCrashLog");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Error sending email: {ex.Message}");

                    await Shell.Current.DisplayAlert("Error", "Could not open
                     email app.", "OK");
                }
            });
        }
        else
        {
            await Shell.Current.DisplayAlert("No Crash Logs", "There are no 
            crash logs to send.", "OK");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice all the attributes available in the DeviceInfo API! I selected these and found them the most helpful, but there are several more options available. More information is available here,
Device Information

🔥 Testing Crashes

Important
Make sure you have an email client set up on your testing device.

Test on physical devices - For that reason, testing crashes isn't really feasible on an emulator, unless you have an email client set up on them.

  1. Force a test crash by navigating to the page with the Test Crash button, press it and your app should crash!
  2. Restart the app
  3. Press "Send Crash Log" button. The email client should pop up and format the message for you.
  4. Verify that the email includes the log as an attachment.
  5. Rebuild and restart the app

The email attachment will contain whatever you included in the Exception message. I typically send the whole stack trace and this narrows down where to look for the bug! Hope this helps!

Top comments (0)