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..06277770 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/App.cs @@ -0,0 +1,25 @@ +namespace Exceptionless.SampleMaui; + +public sealed class App : Application { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly SampleDogfoodRunner _dogfoodRunner; + private readonly MainPage _mainPage; + + public App(MainPage mainPage, ExceptionlessClient exceptionlessClient, SampleDogfoodRunner dogfoodRunner) { + _exceptionlessClient = exceptionlessClient; + _dogfoodRunner = dogfoodRunner; + _mainPage = mainPage; + } + + protected override Window CreateWindow(IActivationState? activationState) { + var window = new Window(_mainPage); + _ = _dogfoodRunner.RunIfRequestedAsync(); + + return window; + } + + 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..3003eaa1 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj @@ -0,0 +1,48 @@ + + + + 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 + + + + Platforms\MacCatalyst\Entitlements.plist + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Exceptionless.SampleMaui/MainPage.cs b/samples/Exceptionless.SampleMaui/MainPage.cs new file mode 100644 index 00000000..51910f78 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/MainPage.cs @@ -0,0 +1,208 @@ +using Microsoft.Maui.Controls.Shapes; + +namespace Exceptionless.SampleMaui; + +public sealed class MainPage : ContentPage { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly SampleEventService _sampleEvents; + private readonly Label _statusLabel; + private readonly Label _lastReferenceIdLabel; + private readonly Label _configLabel; + private readonly ActivityIndicator _activityIndicator; + + public MainPage(ExceptionlessClient exceptionlessClient, SampleEventService sampleEvents) { + _exceptionlessClient = exceptionlessClient; + _sampleEvents = sampleEvents; + + Title = "Exceptionless"; + BackgroundColor = Color.FromArgb("#F5F8EF"); + + _statusLabel = new Label { + Text = !exceptionlessClient.Configuration.IsValid + ? "Configure Exceptionless__ApiKey before sending events." + : exceptionlessClient.Configuration.Enabled ? "Ready" : "Exceptionless is disabled.", + FontSize = 16, + MinimumHeightRequest = 24, + TextColor = Color.FromArgb("#314256"), + LineBreakMode = LineBreakMode.WordWrap + }; + + _lastReferenceIdLabel = new Label { + Text = "Last reference id: none", + FontSize = 16, + MinimumHeightRequest = 24, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.TailTruncation + }; + + _configLabel = new Label { + Text = $"Config {SampleEventService.SampleConfigSettingKey}: not loaded", + FontSize = 16, + MinimumHeightRequest = 24, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.TailTruncation + }; + + _activityIndicator = new ActivityIndicator { + IsVisible = false, + HeightRequest = 24, + Color = Color.FromArgb("#548C00") + }; + + Content = BuildContent(); + } + + private View BuildContent() { + var refreshConfigButton = CreateActionButton("Refresh Config", OnRefreshConfigClicked); + 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, 32), + MaximumWidthRequest = 820, + Spacing = 20, + Children = { + new Label { + Text = "Exceptionless MAUI Sample", + FontSize = 32, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#1D2733") + }, + new Label { + Text = "Submit sample events through the core Exceptionless client.", + FontSize = 18, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.WordWrap + }, + new Border { + Stroke = Color.FromArgb("#DCE5CE"), + StrokeThickness = 1, + BackgroundColor = Colors.White, + StrokeShape = new RoundRectangle { CornerRadius = 8 }, + Padding = new Thickness(18), + Content = new VerticalStackLayout { + Spacing = 10, + Children = { + new Label { + Text = "Client", + FontSize = 22, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#1D2733") + }, + new Label { + Text = $"Server: {_exceptionlessClient.Configuration.ServerUrl}", + FontSize = 16, + MinimumHeightRequest = 24, + TextColor = Color.FromArgb("#576575"), + LineBreakMode = LineBreakMode.WordWrap + }, + new Label { + Text = $"Private information: {_exceptionlessClient.Configuration.IncludePrivateInformation}", + FontSize = 16, + MinimumHeightRequest = 24, + TextColor = Color.FromArgb("#576575") + }, + _statusLabel, + _lastReferenceIdLabel, + _configLabel, + _activityIndicator + } + } + }, + new VerticalStackLayout { + Spacing = 12, + Children = { + refreshConfigButton, + sendExceptionButton, + sendLogButton, + trackFeatureButton, + flushButton + } + } + } + } + }; + } + + private Button CreateActionButton(string text, EventHandler clicked) { + var button = new Button { + Text = text, + IsEnabled = _exceptionlessClient.Configuration.IsValid && _exceptionlessClient.Configuration.Enabled, + BackgroundColor = Color.FromArgb("#436F00"), + TextColor = Colors.White, + CornerRadius = 8, + FontAttributes = FontAttributes.Bold, + FontSize = 17, + HeightRequest = 60, + Padding = new Thickness(16, 12) + }; + + button.Clicked += clicked; + return button; + } + + private async void OnRefreshConfigClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Configuration refreshed.", async () => { + await _sampleEvents.RefreshProjectConfigurationAsync(); + SetConfigValue(_sampleEvents.GetSampleConfigValue()); + }); + } + + private async void OnSendExceptionClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Handled exception queued.", () => { + string referenceId = _sampleEvents.SubmitHandledException(); + SetLastReferenceId(referenceId); + return Task.CompletedTask; + }); + } + + private async void OnSendLogClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Warning log queued.", () => { + SetLastReferenceId(_sampleEvents.SubmitWarningLog()); + return Task.CompletedTask; + }); + } + + private async void OnTrackFeatureClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Feature usage queued.", () => { + SetLastReferenceId(_sampleEvents.TrackFeatureUsage()); + return Task.CompletedTask; + }); + } + + private async void OnFlushClicked(object? sender, EventArgs e) { + await RunClientActionAsync("Queue processed.", () => _sampleEvents.FlushQueueAsync()); + } + + private async Task RunClientActionAsync(string successMessage, Func action) { + try { + _activityIndicator.IsVisible = true; + _activityIndicator.IsRunning = true; + _statusLabel.Text = "Working..."; + + await action(); + + _statusLabel.Text = successMessage; + } catch (InvalidOperationException ex) { + _statusLabel.Text = $"Error: {ex.Message}"; + } catch (TaskCanceledException 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}"; + } + + private void SetConfigValue(string value) { + _configLabel.Text = $"Config {SampleEventService.SampleConfigSettingKey}: {value}"; + } +} diff --git a/samples/Exceptionless.SampleMaui/MauiProgram.cs b/samples/Exceptionless.SampleMaui/MauiProgram.cs new file mode 100644 index 00000000..9368fcf2 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/MauiProgram.cs @@ -0,0 +1,39 @@ +using Exceptionless.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Storage; + +namespace Exceptionless.SampleMaui; + +public static class MauiProgram { + public static MauiApp CreateMauiApp() { + var builder = MauiApp.CreateBuilder(); + var exceptionlessClient = ConfigureExceptionlessClient(); + + builder + .UseMauiApp(); + + builder.Services.AddSingleton(exceptionlessClient); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + return builder.Build(); + } + + private static ExceptionlessClient ConfigureExceptionlessClient() { + string appDataDirectory = FileSystem.Current.AppDataDirectory; + var client = ExceptionlessClient.Default; + var config = client.Configuration; + 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.Join(appDataDirectory, "exceptionless-queue")); + config.UseFileLogger(Path.Join(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..cd2059f3 --- /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..9b241a83 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,48 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + __MAUI_DEFAULT_SCENE_CONFIGURATION__ + UISceneDelegateClassName + SceneDelegate + + + + + 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/MacCatalyst/SceneDelegate.cs b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/SceneDelegate.cs new file mode 100644 index 00000000..766c8d74 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/MacCatalyst/SceneDelegate.cs @@ -0,0 +1,7 @@ +using Foundation; + +namespace Exceptionless.SampleMaui; + +[Register("SceneDelegate")] +public class SceneDelegate : MauiUISceneDelegate { +} 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..b984b238 --- /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..91b17e17 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/iOS/Info.plist @@ -0,0 +1,54 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + __MAUI_DEFAULT_SCENE_CONFIGURATION__ + UISceneDelegateClassName + SceneDelegate + + + + + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + 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/Platforms/iOS/SceneDelegate.cs b/samples/Exceptionless.SampleMaui/Platforms/iOS/SceneDelegate.cs new file mode 100644 index 00000000..766c8d74 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Platforms/iOS/SceneDelegate.cs @@ -0,0 +1,7 @@ +using Foundation; + +namespace Exceptionless.SampleMaui; + +[Register("SceneDelegate")] +public class SceneDelegate : MauiUISceneDelegate { +} diff --git a/samples/Exceptionless.SampleMaui/README.md b/samples/Exceptionless.SampleMaui/README.md new file mode 100644 index 00000000..a565368e --- /dev/null +++ b/samples/Exceptionless.SampleMaui/README.md @@ -0,0 +1,42 @@ +# Exceptionless MAUI Sample + +This sample uses the core `Exceptionless` client from a .NET MAUI app. It configures `ExceptionlessClient.Default`, registers that same instance in MAUI dependency injection, and submits handled exceptions, log events, and feature-usage events from the main page. + +## Configuration + +Set your project's API key before launch. The sample uses the SDK's default hosted services unless you explicitly configure a self-hosted or development server: + +```bash +export Exceptionless__ApiKey="YOUR_API_KEY" +# Optional, for a self-hosted or development server: +# export Exceptionless__ServerUrl="YOUR_SERVER_URL" +``` + +For device launches without environment variables, load the API key from your app's configuration or secure storage and assign it to `ExceptionlessClient.Default.Configuration.ApiKey` before `Startup()`. Keep project keys out of committed source. The sample has no embedded API key or development address. The SDK reads its standard environment variables during `Startup()`; no sample-specific configuration adapter is needed. Invalid configuration or a disabled client disables the page actions and makes autorun report failure. + +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. The SDK still attaches a persistent installation ID; the shared Apple privacy manifest declares that identifier and the linked diagnostic/usage events without advertising tracking. + +The client's default duplicate checker can delay repeated identical events for up to a minute and combine their occurrence counts. **Flush Queue** processes events already in the queue; it does not bypass duplicate checking. + +Use **Refresh Config** to force a project configuration fetch. The page shows the `SampleMaui.ConfigValue` server setting after it is loaded. + +For command-line dogfooding, set `EXCEPTIONLESS_SAMPLE_AUTORUN=true` and optionally `EXCEPTIONLESS_SAMPLE_AUTORUN_RESULT_PATH` before launching the app. Autorun refreshes project configuration, submits a handled exception, submits a warning log, tracks feature usage, flushes the queue, and writes a small result file when a result path is supplied. + +## Build And Run + +Install the MAUI workload for the .NET SDK used by this repository, then build a target supported by your machine: + +```bash +dotnet workload install maui --version 10.0.401 +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 +``` + +Launch the Mac Catalyst target from the command line with: + +```bash +dotnet build samples/Exceptionless.SampleMaui/Exceptionless.SampleMaui.csproj -t:Run -f net10.0-maccatalyst +``` + +For local development, set `Exceptionless__ServerUrl` to an address reachable from the target device or emulator. Android emulators require the host alias described in the [MAUI local web services guidance](https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-10.0); physical devices require a reachable host address. Prefer HTTPS for remote servers. The iOS sample permits local HTTP connections through `NSAllowsLocalNetworking`. diff --git a/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg new file mode 100644 index 00000000..8ab17960 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.png b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.png new file mode 100644 index 00000000..bda74f6c Binary files /dev/null and b/samples/Exceptionless.SampleMaui/Resources/AppIcon/appiconfg.png differ diff --git a/samples/Exceptionless.SampleMaui/Resources/PrivacyInfo.xcprivacy b/samples/Exceptionless.SampleMaui/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..e831fc06 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,87 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + diff --git a/samples/Exceptionless.SampleMaui/SampleDogfoodRunner.cs b/samples/Exceptionless.SampleMaui/SampleDogfoodRunner.cs new file mode 100644 index 00000000..06b7c329 --- /dev/null +++ b/samples/Exceptionless.SampleMaui/SampleDogfoodRunner.cs @@ -0,0 +1,57 @@ +namespace Exceptionless.SampleMaui; + +public sealed class SampleDogfoodRunner { + private readonly SampleEventService _sampleEvents; + private readonly ExceptionlessClient _exceptionlessClient; + + public SampleDogfoodRunner(SampleEventService sampleEvents, ExceptionlessClient exceptionlessClient) { + _sampleEvents = sampleEvents; + _exceptionlessClient = exceptionlessClient; + } + + public async Task RunIfRequestedAsync() { + if (!String.Equals(Environment.GetEnvironmentVariable("EXCEPTIONLESS_SAMPLE_AUTORUN"), "true", StringComparison.OrdinalIgnoreCase)) + return; + + string? resultPath = Environment.GetEnvironmentVariable("EXCEPTIONLESS_SAMPLE_AUTORUN_RESULT_PATH"); + + if (!_exceptionlessClient.Configuration.IsValid || !_exceptionlessClient.Configuration.Enabled) { + await WriteResultAsync(resultPath, "status=failed", "error=Exceptionless configuration is invalid or the client is disabled."); + return; + } + + try { + await _sampleEvents.RefreshProjectConfigurationAsync(); + string configValue = _sampleEvents.GetSampleConfigValue(); + string exceptionReferenceId = _sampleEvents.SubmitHandledException(); + string logReferenceId = _sampleEvents.SubmitWarningLog(); + string featureReferenceId = _sampleEvents.TrackFeatureUsage(); + await _sampleEvents.FlushQueueAsync(); + + await WriteResultAsync(resultPath, + "status=completed", + $"config.{SampleEventService.SampleConfigSettingKey}={configValue}", + $"handledExceptionReferenceId={exceptionReferenceId}", + $"logReferenceId={logReferenceId}", + $"featureReferenceId={featureReferenceId}"); + } catch (InvalidOperationException ex) { + await WriteResultAsync(resultPath, "status=failed", $"error={ex.Message}"); + } catch (TaskCanceledException ex) { + await WriteResultAsync(resultPath, "status=failed", $"error={ex.Message}"); + } + } + + private static async Task WriteResultAsync(string? path, params string[] lines) { + if (String.IsNullOrWhiteSpace(path)) + return; + + // Autorun should still exercise the client if a platform sandbox rejects the result path. + try { + await File.WriteAllLinesAsync(path, lines); + } catch (IOException ex) { + System.Diagnostics.Debug.WriteLine($"Unable to write Exceptionless MAUI sample dogfood result: {ex}"); + } catch (UnauthorizedAccessException ex) { + System.Diagnostics.Debug.WriteLine($"Unable to write Exceptionless MAUI sample dogfood result: {ex}"); + } + } +} diff --git a/samples/Exceptionless.SampleMaui/SampleEventService.cs b/samples/Exceptionless.SampleMaui/SampleEventService.cs new file mode 100644 index 00000000..40e91d6b --- /dev/null +++ b/samples/Exceptionless.SampleMaui/SampleEventService.cs @@ -0,0 +1,62 @@ +using Exceptionless.Configuration; +using Exceptionless.Logging; + +namespace Exceptionless.SampleMaui; + +public sealed class SampleEventService { + public const string SampleConfigSettingKey = "SampleMaui.ConfigValue"; + + private readonly ExceptionlessClient _exceptionlessClient; + + public SampleEventService(ExceptionlessClient exceptionlessClient) { + _exceptionlessClient = exceptionlessClient; + } + + public string SubmitHandledException() { + string referenceId = Guid.NewGuid().ToString("N"); + + try { + throw new InvalidOperationException("Exceptionless MAUI sample handled exception."); + } catch (InvalidOperationException ex) { + ex.ToExceptionless(_exceptionlessClient) + .SetReferenceId(referenceId) + .AddTags("handled") + .SetProperty("Screen", nameof(MainPage)) + .Submit(); + } + + return referenceId; + } + + public string SubmitWarningLog() { + string referenceId = Guid.NewGuid().ToString("N"); + + _exceptionlessClient.CreateLog("Exceptionless.SampleMaui.MainPage", "MAUI sample warning log.", LogLevel.Warn) + .SetReferenceId(referenceId) + .Submit(); + + return referenceId; + } + + public string TrackFeatureUsage() { + string referenceId = Guid.NewGuid().ToString("N"); + + _exceptionlessClient.CreateFeatureUsage("MauiSample.TrackFeature") + .SetReferenceId(referenceId) + .Submit(); + + return referenceId; + } + + public async Task RefreshProjectConfigurationAsync() { + await SettingsManager.UpdateSettingsAsync(_exceptionlessClient.Configuration, 0); + } + + public Task FlushQueueAsync() { + return _exceptionlessClient.ProcessQueueAsync(); + } + + public string GetSampleConfigValue() { + return _exceptionlessClient.Configuration.Settings.GetString(SampleConfigSettingKey, "not loaded"); + } +}