MAUI uses the .NET SDK, which targets netstandard2.0 and is the same package a WPF, WinForms, Avalonia or console app would use. See measuring an app for what it reports. What is MAUI-specific is where you start it, where you put its queue, and how screens get their names.
The source, and a demo desktop app that reports itself, are at skomi-sdk-dotnet.
One client, for the life of the app
Register it in MauiProgram.cs so the app has exactly one:
builder.Services.AddSingleton(_ => new SkomiClient(new SkomiOptions
{
AppId = Guid.Parse("your-app-id"), // from My Apps in Skomi
AppVersion = AppInfo.Current.VersionString,
StoragePath = FileSystem.Current.AppDataDirectory,
}));
AppVersion defaults to the entry assembly's version, which on MAUI is not the version you shipped to a store. AppInfo.Current.VersionString is.
StoragePath decides where the offline queue lives. FileSystem.Current.AppDataDirectory is app-private on every MAUI target and survives an update, which the default local-application-data path does not reliably do on mobile.
⚠ Dispose it on shutdown. That stops the timer and makes one last bounded attempt to deliver; whatever does not go now is on disk and goes on the next run. If you registered it as a singleton, the DI container disposes it for you when the host shuts down, but a MAUI app is often killed rather than shut down, which is the reason the queue exists at all.
Name your screens
skomi.Screen("checkout/review", new Dictionary<string, object?> { ["tier"] = "gold" });
There is no automatic capture of page names. A page class is what your code calls a screen, not what you would call it in a report. The natural place in MAUI is one handler on navigation:
Shell.Current.Navigated += (_, e) =>
skomi.Screen(e.Current.Location.OriginalString.Trim('/'));
Which is a starting point rather than a rule: route strings make readable names only if your routes are readable.
Errors
try
{
await CheckoutAsync();
}
catch (Exception ex)
{
skomi.CaptureError(ex, fatal: false, screenName: "checkout/review");
ShowRetry();
}
Faults are grouped by type rather than by message, because a message carries ids and amounts that make every occurrence unique.
⚠ This is not a crash reporter. It records the faults you hand it and the managed exceptions it can see; a native crash on Android or iOS takes the process with it before anything can be written. Keep the store's own reporting, or Crashlytics or Sentry, for those. What Skomi adds is a crash-free rate beside your release numbers, and the errors you chose to report.
There is also no mapping-file upload, so a stack from a minified release build arrives as it was thrown.
It never throws into your code
Every public method swallows and reports through OnError, which is silent by default: right for a shipped app and wrong while you are integrating:
OnError = (ex, stage) => Debug.WriteLine($"Skomi {stage}: {ex}"),
Waiting for consent
new SkomiOptions { AppId = id, Enabled = false }
Enabled = false records nothing and sends nothing, so a client can exist from start-up and begin collecting only once somebody has agreed.
Check it is working
Run the app, move between two pages, and look at your dashboard. If nothing arrives, set OnError first. An app ID from the wrong app and a client that was never disposed are the two usual causes, and both are silent without it.