diff --git a/README.md b/README.md
index b1f8633..5bb691a 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,13 @@ Dedykowana usługa Windows zastępująca proces Node na stanowiskach z enova365.
- Operacje Enovy są wykonywane pojedynczo i w osobnych sesjach. Zapytania SQL korzystają z puli połączeń i mogą działać równolegle.
- ZO jest zapisywane transakcyjnie, domyślnie do bufora. Powtarzający się numer obcy nie jest blokowany.
- Pierwsza wersja obsługuje PLN, `price_netto` i rabat procentowy `0-100`.
+- Sentry raportuje błędy krytyczne procesu oraz nieoczekiwane błędy procedur Enovy i SQL, oznaczone portalem i nazwą procedury WAMP.
## Konfiguracja
-Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora wraz z hasłem, connection string SQL, definicję dokumentu, magazyn domyślny i tryb zapisu. Repozytorium oraz paczka zawierają tylko `config/enova.json.sample`. Skrypt instalacyjny kopiuje wzorzec do `config/enova.json` wyłącznie wtedy, gdy lokalna konfiguracja jeszcze nie istnieje.
+Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora wraz z hasłem, connection string SQL, definicję dokumentu, magazyn domyślny, tryb zapisu oraz DSN Sentry. Repozytorium oraz paczka zawierają tylko `config/enova.json.sample`. Skrypt instalacyjny kopiuje wzorzec do `config/enova.json` wyłącznie wtedy, gdy lokalna konfiguracja jeszcze nie istnieje.
-Uzupełnij pola `enova.operator`, `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Jeżeli operator lub hasło Enovy nie są podane, usługa nie loguje się do bazy Enovy i nie rejestruje endpointu `eu.smartb2b.place_order`; endpoint SQL oraz diagnostyka pozostają dostępne. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą.
+Uzupełnij pola `enova.operator`, `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Parametr `sentry.dsn` wskazuje projekt Sentry; pusty lub pominięty DSN wyłącza wysyłanie błędów. Jeżeli operator lub hasło Enovy nie są podane, usługa nie loguje się do bazy Enovy i nie rejestruje endpointu `eu.smartb2b.place_order`; endpoint SQL oraz diagnostyka pozostają dostępne. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą.
## Budowanie i testy
diff --git a/daemon/smartb2bsync-enova.xml b/daemon/smartb2bsync-enova.xml
index 2a10562..1232ad7 100644
--- a/daemon/smartb2bsync-enova.xml
+++ b/daemon/smartb2bsync-enova.xml
@@ -32,6 +32,7 @@
+
diff --git a/release/Sentry.dll b/release/Sentry.dll
new file mode 100644
index 0000000..1671d8b
Binary files /dev/null and b/release/Sentry.dll differ
diff --git a/release/SmartB2B.Enova.Contracts.dll b/release/SmartB2B.Enova.Contracts.dll
index 47b969b..0e99549 100644
Binary files a/release/SmartB2B.Enova.Contracts.dll and b/release/SmartB2B.Enova.Contracts.dll differ
diff --git a/release/SmartB2B.Enova.Contracts.pdb b/release/SmartB2B.Enova.Contracts.pdb
index 1a3ce34..c4ca4b6 100644
Binary files a/release/SmartB2B.Enova.Contracts.pdb and b/release/SmartB2B.Enova.Contracts.pdb differ
diff --git a/release/SmartB2B.Enova.Service.deps.json b/release/SmartB2B.Enova.Service.deps.json
index db31af5..8e6b7c9 100644
--- a/release/SmartB2B.Enova.Service.deps.json
+++ b/release/SmartB2B.Enova.Service.deps.json
@@ -9,6 +9,7 @@
"SmartB2B.Enova.Service/1.0.0": {
"dependencies": {
"Newtonsoft.Json": "13.0.3",
+ "Sentry": "6.9.0",
"SmartB2B.Enova.Contracts": "1.0.0",
"WampSharp.Default.Client": "23.8.1"
},
@@ -57,6 +58,14 @@
}
}
},
+ "Sentry/6.9.0": {
+ "runtime": {
+ "lib/net8.0/Sentry.dll": {
+ "assemblyVersion": "6.9.0.0",
+ "fileVersion": "6.9.0.0"
+ }
+ }
+ },
"System.Collections/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
@@ -396,6 +405,13 @@
"path": "newtonsoftmessagepack/0.1.11",
"hashPath": "newtonsoftmessagepack.0.1.11.nupkg.sha512"
},
+ "Sentry/6.9.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-qQIvEwuvjAB6fDLVLLcDj/5f8n5jOyPyHjj3a/GQ1ogTLLQqsSxgQj1fEEquNT9HQuj4ZTyCg3c1DCBMUIvJGQ==",
+ "path": "sentry/6.9.0",
+ "hashPath": "sentry.6.9.0.nupkg.sha512"
+ },
"System.Collections/4.3.0": {
"type": "package",
"serviceable": true,
diff --git a/release/SmartB2B.Enova.Service.dll b/release/SmartB2B.Enova.Service.dll
index 132d6d3..6f7e9c5 100644
Binary files a/release/SmartB2B.Enova.Service.dll and b/release/SmartB2B.Enova.Service.dll differ
diff --git a/release/SmartB2B.Enova.Service.exe b/release/SmartB2B.Enova.Service.exe
index ceb7c37..38494e5 100644
Binary files a/release/SmartB2B.Enova.Service.exe and b/release/SmartB2B.Enova.Service.exe differ
diff --git a/release/SmartB2B.Enova.Service.pdb b/release/SmartB2B.Enova.Service.pdb
index c5b9055..a308ada 100644
Binary files a/release/SmartB2B.Enova.Service.pdb and b/release/SmartB2B.Enova.Service.pdb differ
diff --git a/release/adapters/SmartB2B.Enova.Adapter.dll b/release/adapters/SmartB2B.Enova.Adapter.dll
index 8eaf0d9..36d62cc 100644
Binary files a/release/adapters/SmartB2B.Enova.Adapter.dll and b/release/adapters/SmartB2B.Enova.Adapter.dll differ
diff --git a/release/config/enova.json.sample b/release/config/enova.json.sample
index 0754aa2..4368348 100644
--- a/release/config/enova.json.sample
+++ b/release/config/enova.json.sample
@@ -17,6 +17,9 @@
"connectionString": "Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=True",
"commandTimeoutSeconds": 30
},
+ "sentry": {
+ "dsn": "https://9ae7cbe5e29f4a85b2a748a78e55a868@glitchtip.glebocka.synology.me/10"
+ },
"diagnostics": {
"logDirectory": "daemon",
"logPrefix": "smartb2bsync-enova"
diff --git a/release/daemon/smartb2bsync-enova.xml b/release/daemon/smartb2bsync-enova.xml
index 2a10562..1232ad7 100644
--- a/release/daemon/smartb2bsync-enova.xml
+++ b/release/daemon/smartb2bsync-enova.xml
@@ -32,6 +32,7 @@
+
diff --git a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs
index 9334774..28471ed 100644
--- a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs
+++ b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs
@@ -14,6 +14,8 @@ public sealed class ServiceSettings
public SqlSettings Sql { get; init; } = new();
+ public SentrySettings Sentry { get; init; } = new();
+
public DiagnosticsSettings Diagnostics { get; init; } = new();
public static ServiceSettings Load(string path)
@@ -137,6 +139,11 @@ public sealed class SqlSettings
public int CommandTimeoutSeconds { get; init; } = 30;
}
+public sealed class SentrySettings
+{
+ public string? Dsn { get; init; }
+}
+
public sealed class DiagnosticsSettings
{
public string LogDirectory { get; init; } = "daemon";
diff --git a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs
index 1acb221..3cfe779 100644
--- a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs
+++ b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using Sentry;
using SmartB2B.Enova.Contracts;
using SmartB2B.Enova.Service.Rpc;
@@ -73,6 +74,11 @@ public sealed class PlaceOrderHandler
}
catch (EnovaOperationException exception)
{
+ SentrySdk.CaptureException(exception, scope =>
+ {
+ scope.SetTag("rpc.procedure", "eu.smartb2b.place_order");
+ scope.SetTag("wamp.error_uri", exception.ErrorUri);
+ });
throw WampErrorFactory.FromEnova(exception);
}
diff --git a/src/SmartB2B.Enova.Service/Program.cs b/src/SmartB2B.Enova.Service/Program.cs
index 1f97762..307e11d 100644
--- a/src/SmartB2B.Enova.Service/Program.cs
+++ b/src/SmartB2B.Enova.Service/Program.cs
@@ -5,6 +5,7 @@ using SmartB2B.Enova.Service.Orders;
using SmartB2B.Enova.Service.Rpc;
using SmartB2B.Enova.Service.Runtime;
using SmartB2B.Enova.Service.Sql;
+using Sentry;
using WampSharp.V2.Rpc;
namespace SmartB2B.Enova.Service;
@@ -13,6 +14,7 @@ internal static class Program
{
public static async Task Main(string[] args)
{
+ IDisposable? sentry = null;
var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
@@ -25,6 +27,7 @@ internal static class Program
{
var commandLine = CommandLineOptions.Parse(args);
var settings = ServiceSettings.Load(commandLine.ConfigurationPath);
+ sentry = InitializeSentry(settings);
var runtime = settings.ResolveRuntimeSettings();
using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath);
@@ -98,8 +101,34 @@ internal static class Program
catch (Exception exception)
{
Console.Error.WriteLine($"Błąd krytyczny: {exception}");
+ SentrySdk.CaptureException(exception);
+ await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
return 1;
}
+ finally
+ {
+ sentry?.Dispose();
+ }
+ }
+
+ private static IDisposable? InitializeSentry(ServiceSettings settings)
+ {
+ if (string.IsNullOrWhiteSpace(settings.Sentry.Dsn))
+ {
+ Console.WriteLine("Sentry wyłączone (brak parametru sentry.dsn).");
+ return null;
+ }
+
+ var sentry = SentrySdk.Init(options =>
+ {
+ options.Dsn = settings.Sentry.Dsn;
+ options.IsGlobalModeEnabled = true;
+ options.Release = typeof(Program).Assembly.GetName().Version?.ToString();
+ });
+
+ SentrySdk.ConfigureScope(scope => scope.SetTag("portal", settings.Portal));
+ Console.WriteLine("Raportowanie błędów do Sentry włączone.");
+ return sentry;
}
}
diff --git a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs
index 6de62e3..d980304 100644
--- a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs
+++ b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs
@@ -1,6 +1,7 @@
using WampSharp.Core.Serialization;
using WampSharp.V2.Core.Contracts;
using WampSharp.V2.Rpc;
+using Sentry;
namespace SmartB2B.Enova.Service.Rpc;
@@ -99,6 +100,9 @@ public sealed class DelegateRpcOperation : IWampRpcOperation
catch (Exception exception)
{
Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}");
+ SentrySdk.CaptureException(
+ exception,
+ scope => scope.SetTag("rpc.procedure", Procedure));
var details = formatter.Serialize(new Dictionary());
var keywordArguments = formatter.Serialize(new Dictionary
{
diff --git a/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj b/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj
index 22fbe0e..deb9ea8 100644
--- a/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj
+++ b/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs
index 020e2e1..18ac047 100644
--- a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs
+++ b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs
@@ -1,5 +1,6 @@
using System.Data.Common;
using Newtonsoft.Json;
+using Sentry;
using SmartB2B.Enova.Service.Rpc;
namespace SmartB2B.Enova.Service.Sql;
@@ -98,6 +99,11 @@ public sealed class SqlRawHandler
var code = exception.GetType().GetProperty("Number")?.GetValue(exception)
?? exception.ErrorCode;
Console.Error.WriteLine($"Błąd SQL {code}: {exception.Message}");
+ SentrySdk.CaptureException(exception, scope =>
+ {
+ scope.SetTag("rpc.procedure", "eu.smartb2b.sql_raw");
+ scope.SetExtra("sql.error_code", code);
+ });
throw WampErrorFactory.Create(
"eu.smartb2b.sql_error",
exception.Message,
diff --git a/src/SmartB2B.Enova.Service/config/enova.json.sample b/src/SmartB2B.Enova.Service/config/enova.json.sample
index 0754aa2..4368348 100644
--- a/src/SmartB2B.Enova.Service/config/enova.json.sample
+++ b/src/SmartB2B.Enova.Service/config/enova.json.sample
@@ -17,6 +17,9 @@
"connectionString": "Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=True",
"commandTimeoutSeconds": 30
},
+ "sentry": {
+ "dsn": "https://9ae7cbe5e29f4a85b2a748a78e55a868@glitchtip.glebocka.synology.me/10"
+ },
"diagnostics": {
"logDirectory": "daemon",
"logPrefix": "smartb2bsync-enova"
diff --git a/tests/SmartB2B.Enova.Tests/Program.cs b/tests/SmartB2B.Enova.Tests/Program.cs
index 91e303a..86387da 100644
--- a/tests/SmartB2B.Enova.Tests/Program.cs
+++ b/tests/SmartB2B.Enova.Tests/Program.cs
@@ -207,6 +207,9 @@ static Task SecretsComeFromConfigurationFile()
"connectionString": "Server=db;Database=enova;User ID=user;Password=sql-z-pliku",
"commandTimeoutSeconds": 30
},
+ "sentry": {
+ "dsn": "https://public@example.com/1"
+ },
"diagnostics": {
"logDirectory": "C:/logs",
"logPrefix": "test"
@@ -214,8 +217,10 @@ static Task SecretsComeFromConfigurationFile()
}
""");
- var runtime = ServiceSettings.Load(path).ResolveRuntimeSettings();
+ var settings = ServiceSettings.Load(path);
+ var runtime = settings.ResolveRuntimeSettings();
+ AssertEqual("https://public@example.com/1", settings.Sentry.Dsn, "DSN Sentry");
AssertEqual(
"haslo-z-pliku",
runtime.EnovaConfiguration?.Connection.Password,