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;
+ }
+ }
+}