diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..abf45ac --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://elmah.io/', 'https://elmah.io/pricing/'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ec5f7b7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + + - name: Restore + run: nuget restore Elmah.Io.WinUI.sln + + - name: Check for vulnerable packages + run: | + set -e # This will cause the script to exit on the first error + OUTPUT=$(dotnet list package --vulnerable) + echo "$OUTPUT" + if echo "$OUTPUT" | grep -q 'no vulnerable packages'; then + echo "No vulnerable packages found" + else + if echo "$OUTPUT" | grep -q 'vulnerable'; then + echo "Vulnerable packages found" + exit 1 + fi + fi + + - name: Build + run: msbuild Elmah.Io.WinUI.sln /t:Build /p:Configuration=Release /p:Version=5.1.${{ github.run_number }}-pre + + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal + + - name: Pack + run: dotnet pack --configuration Release src/Elmah.Io.WinUI/Elmah.Io.WinUI.csproj /p:Version=5.1.${{ github.run_number }}-pre + + - name: Install dotnet-validate + run: dotnet tool install --global dotnet-validate --version 0.0.1-preview.304 + + - name: Validate + run: dotnet-validate package local src/Elmah.Io.WinUI/bin/Release/Elmah.Io.WinUI.5.1.${{ github.run_number }}-pre.nupkg + + - name: Push to nuget.org + run: dotnet nuget push src/Elmah.Io.WinUI/bin/Release/Elmah.Io.WinUI.5.1.${{ github.run_number }}-pre.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json + if: ${{ github.event_name == 'push' }} diff --git a/Elmah.Io.WinUI.sln b/Elmah.Io.WinUI.sln new file mode 100644 index 0000000..e151aab --- /dev/null +++ b/Elmah.Io.WinUI.sln @@ -0,0 +1,98 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34916.146 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EE52BC71-E006-4D4F-8353-C4A4BBDBF255}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9A2AC3AB-5D26-4BDE-BD9C-F6E01A8600DE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{37464AD4-C968-41A3-A19E-BD34AA22F175}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elmah.Io.WinUI", "src\Elmah.Io.WinUI\Elmah.Io.WinUI.csproj", "{BAA888FB-705A-4A56-BBE8-6664326C73E2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elmah.Io.WinUI.Sample", "samples\Elmah.Io.WinUI.Sample\Elmah.Io.WinUI.Sample.csproj", "{E8575D6E-3447-4317-8105-0D97EFFA3945}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elmah.Io.WinUI.Test", "test\Elmah.Io.WinUI.Test\Elmah.Io.WinUI.Test.csproj", "{AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|ARM64.Build.0 = Debug|ARM64 + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|x64.ActiveCfg = Debug|x64 + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|x64.Build.0 = Debug|x64 + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Debug|x86.Build.0 = Debug|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|Any CPU.Build.0 = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|ARM64.ActiveCfg = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|ARM64.Build.0 = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|x64.ActiveCfg = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|x64.Build.0 = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|x86.ActiveCfg = Release|Any CPU + {BAA888FB-705A-4A56-BBE8-6664326C73E2}.Release|x86.Build.0 = Release|Any CPU + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|Any CPU.ActiveCfg = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|Any CPU.Build.0 = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|Any CPU.Deploy.0 = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|ARM64.Build.0 = Debug|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x64.ActiveCfg = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x64.Build.0 = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x64.Deploy.0 = Debug|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x86.ActiveCfg = Debug|x86 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x86.Build.0 = Debug|x86 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Debug|x86.Deploy.0 = Debug|x86 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|Any CPU.ActiveCfg = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|Any CPU.Build.0 = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|Any CPU.Deploy.0 = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|ARM64.ActiveCfg = Release|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|ARM64.Build.0 = Release|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|ARM64.Deploy.0 = Release|ARM64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x64.ActiveCfg = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x64.Build.0 = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x64.Deploy.0 = Release|x64 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x86.ActiveCfg = Release|x86 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x86.Build.0 = Release|x86 + {E8575D6E-3447-4317-8105-0D97EFFA3945}.Release|x86.Deploy.0 = Release|x86 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|Any CPU.ActiveCfg = Debug|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|Any CPU.Build.0 = Debug|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|ARM64.Build.0 = Debug|ARM64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|x64.ActiveCfg = Debug|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|x64.Build.0 = Debug|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|x86.ActiveCfg = Debug|x86 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Debug|x86.Build.0 = Debug|x86 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|Any CPU.ActiveCfg = Release|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|Any CPU.Build.0 = Release|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|ARM64.ActiveCfg = Release|ARM64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|ARM64.Build.0 = Release|ARM64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|x64.ActiveCfg = Release|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|x64.Build.0 = Release|x64 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|x86.ActiveCfg = Release|x86 + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BAA888FB-705A-4A56-BBE8-6664326C73E2} = {EE52BC71-E006-4D4F-8353-C4A4BBDBF255} + {E8575D6E-3447-4317-8105-0D97EFFA3945} = {9A2AC3AB-5D26-4BDE-BD9C-F6E01A8600DE} + {AC9D6790-774D-4447-9B55-EF3DBAF9E1AD} = {37464AD4-C968-41A3-A19E-BD34AA22F175} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {74377C86-B599-4978-9556-F5332234CCC4} + EndGlobalSection +EndGlobal diff --git a/samples/Elmah.Io.WinUI.Sample/App.xaml b/samples/Elmah.Io.WinUI.Sample/App.xaml new file mode 100644 index 0000000..d742714 --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/samples/Elmah.Io.WinUI.Sample/App.xaml.cs b/samples/Elmah.Io.WinUI.Sample/App.xaml.cs new file mode 100644 index 0000000..9ece89b --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/App.xaml.cs @@ -0,0 +1,52 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.UI.Xaml.Shapes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Elmah.Io.WinUI.Sample +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + ElmahIoWinUI.Init(new ElmahIoWinUIOptions("API_KEY", new Guid("LOG_ID"))); + + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + m_window = new MainWindow(); + m_window.Activate(); + } + + private Window m_window; + } +} diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/LockScreenLogo.scale-200.png b/samples/Elmah.Io.WinUI.Sample/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/LockScreenLogo.scale-200.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/SplashScreen.scale-200.png b/samples/Elmah.Io.WinUI.Sample/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..32f486a Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/SplashScreen.scale-200.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/Square150x150Logo.scale-200.png b/samples/Elmah.Io.WinUI.Sample/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..53ee377 Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/Square150x150Logo.scale-200.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.scale-200.png b/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..f713bba Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.scale-200.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..dc9f5be Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/StoreLogo.png b/samples/Elmah.Io.WinUI.Sample/Assets/StoreLogo.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/StoreLogo.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Assets/Wide310x150Logo.scale-200.png b/samples/Elmah.Io.WinUI.Sample/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..8b4a5d0 Binary files /dev/null and b/samples/Elmah.Io.WinUI.Sample/Assets/Wide310x150Logo.scale-200.png differ diff --git a/samples/Elmah.Io.WinUI.Sample/Elmah.Io.WinUI.Sample.csproj b/samples/Elmah.Io.WinUI.Sample/Elmah.Io.WinUI.Sample.csproj new file mode 100644 index 0000000..cebb064 --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/Elmah.Io.WinUI.Sample.csproj @@ -0,0 +1,52 @@ + + + WinExe + net8.0-windows10.0.19041.0 + 10.0.17763.0 + Elmah.Io.WinUI.Sample + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + win10-x86;win10-x64;win10-arm64 + win-$(Platform).pubxml + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + \ No newline at end of file diff --git a/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml b/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml new file mode 100644 index 0000000..d82df92 --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml.cs b/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml.cs new file mode 100644 index 0000000..451c655 --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/MainWindow.xaml.cs @@ -0,0 +1,41 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Elmah.Io.WinUI.Sample +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + } + + private void myButton_Click(object sender, RoutedEventArgs e) + { + // Comment out to add a breadcrumb before the error + //ElmahIoWinUI.AddBreadcrumb(new Client.Breadcrumb(DateTimeOffset.Now, "Information", "Click", "User clicking error button")); + + myButton.Content = "Clicked"; + + throw new ApplicationException("Oh no"); + } + } +} diff --git a/samples/Elmah.Io.WinUI.Sample/Package.appxmanifest b/samples/Elmah.Io.WinUI.Sample/Package.appxmanifest new file mode 100644 index 0000000..9edf5d9 --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + Elmah.Io.WinUI.Sample + thoma + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Elmah.Io.WinUI.Sample/Properties/launchSettings.json b/samples/Elmah.Io.WinUI.Sample/Properties/launchSettings.json new file mode 100644 index 0000000..974009d --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Elmah.Io.WinUI.Sample (Package)": { + "commandName": "MsixPackage" + }, + "Elmah.Io.WinUI.Sample (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/samples/Elmah.Io.WinUI.Sample/app.manifest b/samples/Elmah.Io.WinUI.Sample/app.manifest new file mode 100644 index 0000000..c317c3e --- /dev/null +++ b/samples/Elmah.Io.WinUI.Sample/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/Elmah.Io.WinUI/Elmah.Io.WinUI.csproj b/src/Elmah.Io.WinUI/Elmah.Io.WinUI.csproj new file mode 100644 index 0000000..547fb7a --- /dev/null +++ b/src/Elmah.Io.WinUI/Elmah.Io.WinUI.csproj @@ -0,0 +1,40 @@ + + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + win-x86;win-x64;win-arm64 + x86;x64;ARM64 + enable + 12.0 + + Logging to elmah.io from Windows UI (WinUI) + elmah.io + Elmah.Io.WinUI + Elmah.Io.WinUI + Error;Exception;Reporting;Management;Logging;ELMAH;Diagnostics;Tracing;WinUI + https://secure.gravatar.com/avatar/5c4cb3646528821117abde6d2d5ee22d?s=64 + https://secure.gravatar.com/avatar/5c4cb3646528821117abde6d2d5ee22d?s=64 + icon.png + https://elmah.io + https://github.com/elmahio/Elmah.Io.WinUI + git + Apache-2.0 + false + false + false + true + README.md + true + + true + true + embedded + + + + + + + + diff --git a/src/Elmah.Io.WinUI/ElmahIoWinUI.cs b/src/Elmah.Io.WinUI/ElmahIoWinUI.cs new file mode 100644 index 0000000..9f6bfd1 --- /dev/null +++ b/src/Elmah.Io.WinUI/ElmahIoWinUI.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Elmah.Io.Client; +using System.Linq; +using System.Security.Principal; +using System.Net.Http.Headers; +using Microsoft.UI.Xaml; + +namespace Elmah.Io.WinUI +{ + /// + /// Main class used to interact with the elmah.io API from WinUI. + /// + public static class ElmahIoWinUI + { + internal static readonly string _assemblyVersion = typeof(ElmahIoWinUI).Assembly.GetName().Version?.ToString() ?? "Unknown"; + internal static readonly string _winUiAssemblyVersion = typeof(Application).Assembly.GetName().Version?.ToString() ?? "Unknown"; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + private static ElmahIoWinUIOptions _options; + private static IElmahioAPI _logger; + private static List _breadcrumbs; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + /// + /// Initialize logging of all uncaught errors to elmah.io. + /// + public static void Init(ElmahIoWinUIOptions options) + { + if (options == null) throw new ArgumentNullException(nameof(options)); +#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one + if (string.IsNullOrWhiteSpace(options.ApiKey)) throw new ArgumentNullException(nameof(options.ApiKey)); +#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one + if (options.LogId == Guid.Empty) throw new ArgumentException(nameof(options.LogId)); + + _options = options; + _breadcrumbs = new List(1 + options.MaximumBreadcrumbs); + _logger = ElmahioAPI.Create(options.ApiKey, new ElmahIoOptions + { + Timeout = new TimeSpan(0, 0, 5), + UserAgent = UserAgent(), + }); + + _logger.Messages.OnMessageFail += (sender, args) => + { + options.OnError?.Invoke(args.Message, args.Error); + }; + + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + Log(args.ExceptionObject as Exception); + + TaskScheduler.UnobservedTaskException += (sender, args) => + Log(args.Exception); + + Application.Current.UnhandledException += (sender, args) => + Log(args.Exception); + } + + /// + /// Log an exception to elmah.io manually. + /// + public static void Log(Exception? exception) + { + var baseException = exception?.GetBaseException(); + var createMessage = new CreateMessage + { + DateTime = DateTime.UtcNow, + Detail = exception?.ToString(), + Type = baseException?.GetType().FullName, + Title = baseException?.Message ?? "An error occurred", + Data = exception?.ToDataList(), + Severity = "Error", + Source = baseException?.Source, + User = WindowsIdentity.GetCurrent().Name, + Hostname = Hostname(), + Breadcrumbs = Breadcrumbs(), + Application = _options.Application, + ServerVariables = + [ + new("User-Agent", $"X-ELMAHIO-APPLICATION; OS=Windows; OSVERSION={Environment.OSVersion.Version}; ENGINE=WinUI"), + ] + }; + + if (_options.OnFilter != null && _options.OnFilter(createMessage)) + { + return; + } + + _options.OnMessage?.Invoke(createMessage); + + try + { + _logger.Messages.Create(_options.LogId.ToString(), createMessage); + } + catch (Exception ex) + { + _options.OnError?.Invoke(createMessage, ex); + } + } + + /// + /// Add a breadcrumb in-memory. Breadcrumbs will be added to errors when logged + /// either automatically or manually. + /// + public static void AddBreadcrumb(Breadcrumb breadcrumb) + { + _breadcrumbs.Add(breadcrumb); + + if (_breadcrumbs.Count >= _options.MaximumBreadcrumbs) + { + var oldest = _breadcrumbs.OrderBy(b => b.DateTime).First(); + _breadcrumbs.Remove(oldest); + } + } + + private static string? Hostname() + { + var machineName = Environment.MachineName; + if (!string.IsNullOrWhiteSpace(machineName)) return machineName; + + return Environment.GetEnvironmentVariable("COMPUTERNAME"); + } + + private static List Breadcrumbs() + { + if (_breadcrumbs == null || _breadcrumbs.Count == 0) return []; + + var utcNow = DateTime.UtcNow; + + // Set default values on properties not set + foreach (var breadcrumb in _breadcrumbs) + { + if (!breadcrumb.DateTime.HasValue) breadcrumb.DateTime = utcNow; + if (string.IsNullOrWhiteSpace(breadcrumb.Severity)) breadcrumb.Severity = "Information"; + if (string.IsNullOrWhiteSpace(breadcrumb.Action)) breadcrumb.Action = "Log"; + } + + var breadcrumbs = _breadcrumbs.OrderByDescending(l => l.DateTime).ToList(); + _breadcrumbs.Clear(); + return breadcrumbs; + } + + private static string UserAgent() + { + return new StringBuilder() + .Append(new ProductInfoHeaderValue(new ProductHeaderValue("Elmah.Io.WinUI", _assemblyVersion)).ToString()) + .Append(' ') + .Append(new ProductInfoHeaderValue(new ProductHeaderValue("Microsoft.WinUI", _winUiAssemblyVersion)).ToString()) + .ToString(); + } + } +} diff --git a/src/Elmah.Io.WinUI/ElmahIoWinUIOptions.cs b/src/Elmah.Io.WinUI/ElmahIoWinUIOptions.cs new file mode 100644 index 0000000..333c62d --- /dev/null +++ b/src/Elmah.Io.WinUI/ElmahIoWinUIOptions.cs @@ -0,0 +1,50 @@ +using Elmah.Io.Client; +using System; + +namespace Elmah.Io.WinUI +{ + /// + /// Options for setting up elmah.io logging from WinUI. + /// + public class ElmahIoWinUIOptions(string apiKey, Guid logId) + { + /// + /// The API key from the elmah.io UI. + /// + public string ApiKey { get; set; } = apiKey; + + /// + /// The id of the log to send messages to. + /// + public Guid LogId { get; set; } = logId; + + /// + /// An application name to put on all error messages. + /// + public string? Application { get; set; } + + /// + /// Register an action to be called before logging an error. Use the OnMessage action to + /// decorate error messages with additional information. + /// + public Action? OnMessage { get; set; } + + /// + /// Register an action to be called if communicating with the elmah.io API fails. + /// You can use this callback to log the error through which ever logging framework + /// you may use. + /// + public Action? OnError { get; set; } + + /// + /// Register an action to filter log messages. Use this to add client-side ignore + /// of some error messages. If the filter action returns true, the error is ignored. + /// + public Func? OnFilter { get; set; } + + /// + /// The maximum number of breadcrumbs to store in-memory. Default = 10. + /// + public int MaximumBreadcrumbs { get; set; } = 10; + } +} diff --git a/src/Elmah.Io.WinUI/images/icon.png b/src/Elmah.Io.WinUI/images/icon.png new file mode 100644 index 0000000..1999641 Binary files /dev/null and b/src/Elmah.Io.WinUI/images/icon.png differ diff --git a/test/Elmah.Io.WinUI.Test/Elmah.Io.WinUI.Test.csproj b/test/Elmah.Io.WinUI.Test/Elmah.Io.WinUI.Test.csproj new file mode 100644 index 0000000..c2a0967 --- /dev/null +++ b/test/Elmah.Io.WinUI.Test/Elmah.Io.WinUI.Test.csproj @@ -0,0 +1,23 @@ + + + + net8.0-windows10.0.19041.0 + enable + enable + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + win10-x86;win10-x64;win10-arm64 + + + + + + + + + + + + + + diff --git a/test/Elmah.Io.WinUI.Test/ElmahIoWinUITest.cs b/test/Elmah.Io.WinUI.Test/ElmahIoWinUITest.cs new file mode 100644 index 0000000..f2849c6 --- /dev/null +++ b/test/Elmah.Io.WinUI.Test/ElmahIoWinUITest.cs @@ -0,0 +1,66 @@ +using Elmah.Io.Client; +using NSubstitute; +using NUnit.Framework; +using System.Reflection; + +namespace Elmah.Io.WinUI.Test +{ + public class ElmahIoWinUITest + { + [Test] + public void Test() + { + // Arrange + var options = new ElmahIoWinUIOptions("hello", Guid.NewGuid()) + { + Application = "MyApp" + }; + + var optionsField = typeof(ElmahIoWinUI).GetField("_options", BindingFlags.Static | BindingFlags.NonPublic); + optionsField?.SetValue(null, options); + + var messagesClient = Substitute.For(); + var elmahIoClient = Substitute.For(); + elmahIoClient.Messages.Returns(messagesClient); + + var loggerField = typeof(ElmahIoWinUI).GetField("_logger", BindingFlags.Static | BindingFlags.NonPublic); + loggerField?.SetValue(null, elmahIoClient); + + var breadcrumbs = new List(); + var breadcrumbsField = typeof(ElmahIoWinUI).GetField("_breadcrumbs", BindingFlags.Static | BindingFlags.NonPublic); + breadcrumbsField?.SetValue(null, breadcrumbs); + + ElmahIoWinUI.AddBreadcrumb(new Breadcrumb + { + DateTime = DateTime.UtcNow, + Action = "Navigation", + Message = "Opening app", + Severity = "Information", + }); + + var ex = new ApplicationException("Oh no"); + + // Act + ElmahIoWinUI.Log(ex); + + // Assert + messagesClient.Received().Create(Arg.Is(s => s == options.LogId.ToString()), Arg.Is(msg => AssertMessage(msg, ex))); + } + + private static bool AssertMessage(CreateMessage msg, ApplicationException ex) + { + if (msg.Title != "Oh no") return false; + if (msg.Breadcrumbs.Count != 1) return false; + var openingBreadcrumb = msg.Breadcrumbs.First(); + if (openingBreadcrumb.Action != "Navigation" || openingBreadcrumb.Severity != "Information" || openingBreadcrumb.Message != "Opening app") return false; + + if (string.IsNullOrWhiteSpace(msg.Detail)) return false; + if (msg.Type != ex.GetType().FullName) return false; + if (msg.Severity != "Error") return false; + if (msg.Source != ex.Source) return false; + if (msg.Application != "MyApp") return false; + + return true; + } + } +}