diff --git a/Exceptionless.Net.slnx b/Exceptionless.Net.slnx index c177c837..81f0b918 100644 --- a/Exceptionless.Net.slnx +++ b/Exceptionless.Net.slnx @@ -19,6 +19,7 @@ + diff --git a/samples/Exceptionless.SampleMaui/App.cs b/samples/Exceptionless.SampleMaui/App.cs new file mode 100644 index 00000000..00acd6ba --- /dev/null +++ b/samples/Exceptionless.SampleMaui/App.cs @@ -0,0 +1,20 @@ +namespace Exceptionless.SampleMaui; + +public sealed class App : Application { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly MainPage _mainPage; + + public App(MainPage mainPage, ExceptionlessClient exceptionlessClient) { + _exceptionlessClient = exceptionlessClient; + _mainPage = mainPage; + } + + protected override Window CreateWindow(IActivationState? activationState) { + return new Window(_mainPage); + } + + protected override void OnSleep() { + _ = _exceptionlessClient.ProcessQueueAsync(); + base.OnSleep(); + } +} diff --git a/samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj b/samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj new file mode 100644 index 00000000..873fffc3 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj @@ -0,0 +1,40 @@ + + + + net10.0-android + $(TargetFrameworks);net10.0-ios;net10.0-maccatalyst + $(TargetFrameworks);net10.0-windows10.0.19041.0 + Exe + Exceptionless.SampleMaui + true + true + enable + enable + + Exceptionless MAUI Sample + com.exceptionless.samplemaui + 1.0 + 1 + None + + 21.0 + 15.0 + 15.0 + 10.0.17763.0 + 10.0.17763.0 + + + + + + + + + + + + + + + + diff --git a/samples/Exceptionless.SampleMaui/MainPage.cs b/samples/Exceptionless.SampleMaui/MainPage.cs new file mode 100644 index 00000000..79a70f3b --- /dev/null +++ b/samples/Exceptionless.SampleMaui/MainPage.cs @@ -0,0 +1,193 @@ +using Exceptionless.Logging; +using Microsoft.Maui.Controls.Shapes; + +namespace Exceptionless.SampleMaui; + +public sealed class MainPage : ContentPage { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly Label _statusLabel; + private readonly Label _lastReferenceIdLabel; + private readonly ActivityIndicator _activityIndicator; + + public MainPage(ExceptionlessClient exceptionlessClient) { + _exceptionlessClient = exceptionlessClient; + + Title = "Exceptionless"; + BackgroundColor = Color.FromArgb("#F6F8FA"); + + _statusLabel = new Label { + Text = "Ready", + FontSize = 14, + TextColor = Color.FromArgb("#314256"), + LineBreakMode = LineBreakMode.WordWrap + }; + + _lastReferenceIdLabel = new Label { + Text = "Last reference id: none", + FontSize = 13, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.TailTruncation + }; + + _activityIndicator = new ActivityIndicator { + IsVisible = false, + Color = Color.FromArgb("#276749") + }; + + Content = BuildContent(); + } + + private View BuildContent() { + var sendExceptionButton = CreateActionButton("Send Handled Exception", OnSendExceptionClicked); + var sendLogButton = CreateActionButton("Send Warning Log", OnSendLogClicked); + var trackFeatureButton = CreateActionButton("Track Feature", OnTrackFeatureClicked); + var flushButton = CreateActionButton("Flush Queue", OnFlushClicked); + + return new ScrollView { + Content = new VerticalStackLayout { + Padding = new Thickness(24, 28), + Spacing = 18, + Children = { + new Label { + Text = "Exceptionless MAUI Sample", + FontSize = 26, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#1D2733") + }, + new Label { + Text = "Submit sample events through the core Exceptionless client.", + FontSize = 15, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.WordWrap + }, + new Border { + Stroke = Color.FromArgb("#D8DEE6"), + StrokeThickness = 1, + BackgroundColor = Colors.White, + StrokeShape = new RoundRectangle { CornerRadius = 8 }, + Padding = new Thickness(18), + Content = new VerticalStackLayout { + Spacing = 12, + Children = { + new Label { + Text = "Client", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#1D2733") + }, + new Label { + Text = $"Server: {_exceptionlessClient.Configuration.ServerUrl}", + FontSize = 13, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.TailTruncation + }, + new Label { + Text = $"Private information: {_exceptionlessClient.Configuration.IncludePrivateInformation}", + FontSize = 13, + TextColor = Color.FromArgb("#576575") + }, + _statusLabel, + _lastReferenceIdLabel, + _activityIndicator + } + } + }, + new Grid { + ColumnDefinitions = { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + }, + RowDefinitions = { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnSpacing = 12, + RowSpacing = 12, + Children = { + sendExceptionButton, + sendLogButton, + trackFeatureButton, + flushButton + } + } + } + } + }; + } + + private static Button CreateActionButton(string text, EventHandler clicked) { + var button = new Button { + Text = text, + BackgroundColor = Color.FromArgb("#285A84"), + TextColor = Colors.White, + CornerRadius = 8, + FontAttributes = FontAttributes.Bold, + MinimumHeightRequest = 48 + }; + + button.Clicked += clicked; + return button; + } + + private async void OnSendExceptionClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Handled exception queued.", () => { + string referenceId = Guid.NewGuid().ToString("N"); + + try { + throw new InvalidOperationException("Exceptionless MAUI sample handled exception."); + } catch (Exception ex) { + ex.ToExceptionless(_exceptionlessClient) + .SetReferenceId(referenceId) + .AddTags("handled") + .SetProperty("Screen", nameof(MainPage)) + .Submit(); + } + + SetLastReferenceId(referenceId); + return Task.CompletedTask; + }); + } + + private async void OnSendLogClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Warning log queued.", () => { + _exceptionlessClient.SubmitLog("Exceptionless.SampleMaui.MainPage", "MAUI sample warning log.", LogLevel.Warn); + SetLastReferenceId(_exceptionlessClient.GetLastReferenceId()); + return Task.CompletedTask; + }); + } + + private async void OnTrackFeatureClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Feature usage queued.", () => { + _exceptionlessClient.SubmitFeatureUsage("MauiSample.TrackFeature"); + SetLastReferenceId(_exceptionlessClient.GetLastReferenceId()); + return Task.CompletedTask; + }); + } + + private async void OnFlushClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Queue processed.", () => _exceptionlessClient.ProcessQueueAsync()); + } + + private async Task RunClientActionAsync(string successMessage, Func action) { + try { + _activityIndicator.IsVisible = true; + _activityIndicator.IsRunning = true; + _statusLabel.Text = "Working..."; + + await action(); + + _statusLabel.Text = successMessage; + } catch (Exception ex) { + _statusLabel.Text = $"Error: {ex.Message}"; + } finally { + _activityIndicator.IsRunning = false; + _activityIndicator.IsVisible = false; + } + } + + private void SetLastReferenceId(string? referenceId) { + _lastReferenceIdLabel.Text = String.IsNullOrEmpty(referenceId) + ? "Last reference id: none" + : $"Last reference id: {referenceId}"; + } +} diff --git a/samples/Exceptionless.SampleMaui/MauiProgram.cs b/samples/Exceptionless.SampleMaui/MauiProgram.cs new file mode 100644 index 00000000..52dc2669 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/MauiProgram.cs @@ -0,0 +1,43 @@ +using Exceptionless.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Storage; + +namespace Exceptionless.SampleMaui; + +public static class MauiProgram { + private const string DefaultApiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"; + private const string DefaultServerUrl = "https://ex.dev.localhost:7111"; + + public static MauiApp CreateMauiApp() { + var builder = MauiApp.CreateBuilder(); + var exceptionlessClient = CreateExceptionlessClient(); + + builder + .UseMauiApp(); + + builder.Services.AddSingleton(exceptionlessClient); + builder.Services.AddSingleton(); + + return builder.Build(); + } + + private static ExceptionlessClient CreateExceptionlessClient() { + string appDataDirectory = FileSystem.Current.AppDataDirectory; + + var client = new ExceptionlessClient(config => { + config.ApiKey = Environment.GetEnvironmentVariable("EXCEPTIONLESS_API_KEY") ?? DefaultApiKey; + config.ServerUrl = Environment.GetEnvironmentVariable("EXCEPTIONLESS_SERVER_URL") ?? DefaultServerUrl; + config.IncludePrivateInformation = false; + config.DefaultTags.Add("maui"); + config.DefaultTags.Add("sample"); + config.DefaultData["Platform"] = DeviceInfo.Current.Platform.ToString(); + config.DefaultData["DeviceIdiom"] = DeviceInfo.Current.Idiom.ToString(); + config.SetVersion(AppInfo.Current.VersionString); + config.UseFolderStorage(Path.Combine(appDataDirectory, "exceptionless-queue")); + config.UseFileLogger(Path.Combine(appDataDirectory, "exceptionless-client.log"), LogLevel.Info); + }); + + client.Startup(); + return client; + } +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/Android/AndroidManifest.xml b/samples/Exceptionless.SampleMaui/Platforms/Android/AndroidManifest.xml new file mode 100644 index 00000000..29509345 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/Android/MainActivity.cs b/samples/Exceptionless.SampleMaui/Platforms/Android/MainActivity.cs new file mode 100644 index 00000000..6542cf21 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Android/MainActivity.cs @@ -0,0 +1,8 @@ +using Android.App; +using Android.Content.PM; + +namespace Exceptionless.SampleMaui; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity { +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/Android/MainApplication.cs b/samples/Exceptionless.SampleMaui/Platforms/Android/MainApplication.cs new file mode 100644 index 00000000..3c4e0847 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Android/MainApplication.cs @@ -0,0 +1,12 @@ +using Android.App; +using Android.Runtime; + +namespace Exceptionless.SampleMaui; + +[Application] +public class MainApplication : MauiApplication { + public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/AppDelegate.cs b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 00000000..103a1cef --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,8 @@ +using Foundation; + +namespace Exceptionless.SampleMaui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate { + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Entitlements.plist b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Entitlements.plist new file mode 100644 index 00000000..a8c10726 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Info.plist b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Info.plist new file mode 100644 index 00000000..ab9e1df4 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,31 @@ + + + + + UIDeviceFamily + + 2 + + LSApplicationCategoryType + public.app-category.developer-tools + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Program.cs b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Program.cs new file mode 100644 index 00000000..7d22b9f6 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,9 @@ +using UIKit; + +namespace Exceptionless.SampleMaui; + +public static class Program { + private static void Main(string[] args) { + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml b/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml new file mode 100644 index 00000000..36a665ef --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml @@ -0,0 +1,7 @@ + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml.cs b/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml.cs new file mode 100644 index 00000000..71d4791f --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Windows/App.xaml.cs @@ -0,0 +1,11 @@ +using Microsoft.UI.Xaml; + +namespace Exceptionless.SampleMaui.WinUI; + +public partial class App : MauiWinUIApplication { + public App() { + InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/Windows/Package.appxmanifest b/samples/Exceptionless.SampleMaui/Platforms/Windows/Package.appxmanifest new file mode 100644 index 00000000..cfbe3da1 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,41 @@ + + + + + + + Exceptionless MAUI Sample + Exceptionless + appiconStoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/Windows/app.manifest b/samples/Exceptionless.SampleMaui/Platforms/Windows/app.manifest new file mode 100644 index 00000000..b04556c6 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/Windows/app.manifest @@ -0,0 +1,11 @@ + + + + + + true/PM + PerMonitorV2, PerMonitor + true + + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/iOS/AppDelegate.cs b/samples/Exceptionless.SampleMaui/Platforms/iOS/AppDelegate.cs new file mode 100644 index 00000000..103a1cef --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,8 @@ +using Foundation; + +namespace Exceptionless.SampleMaui; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate { + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/samples/Exceptionless.SampleMaui/Platforms/iOS/Info.plist b/samples/Exceptionless.SampleMaui/Platforms/iOS/Info.plist new file mode 100644 index 00000000..f2e00c68 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/samples/Exceptionless.SampleMaui/Platforms/iOS/Program.cs b/samples/Exceptionless.SampleMaui/Platforms/iOS/Program.cs new file mode 100644 index 00000000..7d22b9f6 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/iOS/Program.cs @@ -0,0 +1,9 @@ +using UIKit; + +namespace Exceptionless.SampleMaui; + +public static class Program { + private static void Main(string[] args) { + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/samples/Exceptionless.SampleMaui/README.md b/samples/Exceptionless.SampleMaui/README.md new file mode 100644 index 00000000..dbb321c4 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/README.md @@ -0,0 +1,32 @@ +# Exceptionless MAUI Sample + +This sample uses the core `Exceptionless` client from a .NET MAUI app. There is no MAUI-specific Exceptionless package, so the app registers an `ExceptionlessClient` in MAUI dependency injection and submits handled exceptions, log events, and feature-usage events from the main page. + +## Configuration + +The sample defaults to the same local development server used by the other samples: + +- API key: `LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest` +- Server URL: `https://ex.dev.localhost:7111` + +Override either value with environment variables before launch: + +```bash +export EXCEPTIONLESS_API_KEY="YOUR_API_KEY" +export EXCEPTIONLESS_SERVER_URL="https://collector.exceptionless.io" +``` + +Events are queued under `FileSystem.Current.AppDataDirectory`, `IncludePrivateInformation` is disabled, and the sample has an explicit **Flush Queue** action. The app also asks the client to process the queue when the MAUI application goes to sleep. + +## Run + +Install the MAUI workload for the .NET SDK used by this repository, then run a target supported by your machine: + +```bash +dotnet workload install maui +dotnet build samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj -f net10.0-maccatalyst +dotnet build samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj -f net10.0-ios +dotnet build samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj -f net10.0-android +``` + +Android emulators cannot usually reach a host machine's `localhost` address directly. If you are sending to a local Exceptionless server from Android, use a URL that the emulator can reach and make sure the development certificate is trusted. diff --git a/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg new file mode 100644 index 00000000..4426df4e --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.svg b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.svg new file mode 100644 index 00000000..5f558083 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/Exceptionless.SampleMaui/Resources/Splash/splash.svg b/samples/Exceptionless.SampleMaui/Resources/Splash/splash.svg new file mode 100644 index 00000000..03ca0527 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Resources/Splash/splash.svg @@ -0,0 +1,5 @@ + + + + +