diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e7779d51..3c7b61474a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Features + +- Added `SentrySdk.CrashedLastRun()`. Users can now retrieve the `LastRunState` ([#4025](https://github.com/getsentry/sentry-dotnet/pull/4025)) + ### Fixes - Using SentryOptions.Native.SuppressExcBadAccess and SentryOptions.Native.SuppressSignalAborts, users can now block duplicate errors from native due to dotnet NullReferenceExceptions - Defaults to false ([#3998](https://github.com/getsentry/sentry-dotnet/pull/3998)) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 6c18af6e45..906098cfea 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -197,6 +197,12 @@ public SentryId CaptureCheckIn( Action? configureMonitorOptions = null) => SentryId.Empty; + /// + /// No-Op + /// + public CrashedLastRun CrashedLastRun() + => Sentry.CrashedLastRun.Unknown; + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 61d715d341..7de21df35c 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -287,6 +287,12 @@ public SentryId CaptureCheckIn( Action? monitorOptions = null) => SentrySdk.CaptureCheckIn(monitorSlug, status, sentryId, duration, scope, monitorOptions); + /// + /// Forwards the call to . + /// + public CrashedLastRun CrashedLastRun() + => SentrySdk.CrashedLastRun(); + /// /// Forwards the call to /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 5b8d923f7b..1c132aba91 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -116,4 +116,11 @@ TransactionContext ContinueTrace( /// The callback to configure the scope. /// public SentryId CaptureEvent(SentryEvent evt, SentryHint? hint, Action configureScope); + + /// + /// Retrieves the crash state of the previous application run. + /// This indicates whether the application terminated normally or crashed. + /// + /// indicating the state of the previous run. + public CrashedLastRun CrashedLastRun(); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index cd00686250..aa464f066d 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -679,6 +679,26 @@ public SentryId CaptureCheckIn( return SentryId.Empty; } + + public CrashedLastRun CrashedLastRun() + { + if (!IsEnabled) + { + return Sentry.CrashedLastRun.Unknown; + } + + if (_options.CrashedLastRun is null) + { + _options.DiagnosticLogger?.LogDebug("The SDK does not have a 'CrashedLastRun' set. " + + "This might be due to a missing or disabled native integration."); + return Sentry.CrashedLastRun.Unknown; + } + + return _options.CrashedLastRun.Invoke() + ? Sentry.CrashedLastRun.Crashed + : Sentry.CrashedLastRun.DidNotCrash; + } + public async Task FlushAsync(TimeSpan timeout) { try diff --git a/src/Sentry/LastRunState.cs b/src/Sentry/LastRunState.cs new file mode 100644 index 0000000000..9d5e6e90f9 --- /dev/null +++ b/src/Sentry/LastRunState.cs @@ -0,0 +1,24 @@ +namespace Sentry; + +/// +/// Represents the crash state of the games's previous run. +/// Used to determine if the last execution terminated normally or crashed. +/// +public enum CrashedLastRun +{ + /// + /// The LastRunState is unknown. This might be due to the SDK not being initialized, native crash support + /// missing, or being disabled. + /// + Unknown, + + /// + /// The application did not crash during the last run. + /// + DidNotCrash, + + /// + /// The application crashed during the last run. + /// + Crashed +} diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 401a0fa6f0..6941f26669 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -705,6 +705,11 @@ public static void PauseSession() public static void ResumeSession() => CurrentHub.ResumeSession(); + /// + [DebuggerStepThrough] + public static CrashedLastRun CrashedLastRun() + => CurrentHub.CrashedLastRun(); + /// /// Deliberately crashes an application, which is useful for testing and demonstration purposes. /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 67795954b7..ad5b90c592 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -75,6 +75,12 @@ namespace Sentry ManagedBackgroundThread = 1, Native = 2, } + public enum CrashedLastRun + { + Unknown = 0, + DidNotCrash = 1, + Crashed = 2, + } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] public class Debouncer { @@ -214,6 +220,7 @@ namespace Sentry Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); + Sentry.CrashedLastRun CrashedLastRun(); void EndSession(Sentry.SessionEndStatus status = 0); Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); @@ -841,6 +848,7 @@ namespace Sentry public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public static Sentry.CrashedLastRun CrashedLastRun() { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } public static void Flush() { } public static void Flush(System.TimeSpan timeout) { } @@ -1342,6 +1350,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void Dispose() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -1388,6 +1397,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } public Sentry.BaggageHeader? GetBaggage() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 67795954b7..ad5b90c592 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -75,6 +75,12 @@ namespace Sentry ManagedBackgroundThread = 1, Native = 2, } + public enum CrashedLastRun + { + Unknown = 0, + DidNotCrash = 1, + Crashed = 2, + } [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] public class Debouncer { @@ -214,6 +220,7 @@ namespace Sentry Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); + Sentry.CrashedLastRun CrashedLastRun(); void EndSession(Sentry.SessionEndStatus status = 0); Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); @@ -841,6 +848,7 @@ namespace Sentry public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public static Sentry.CrashedLastRun CrashedLastRun() { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } public static void Flush() { } public static void Flush(System.TimeSpan timeout) { } @@ -1342,6 +1350,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void Dispose() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -1388,6 +1397,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } public Sentry.BaggageHeader? GetBaggage() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 0c06282d34..fc579aaec8 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -74,6 +74,12 @@ namespace Sentry Managed = 0, ManagedBackgroundThread = 1, } + public enum CrashedLastRun + { + Unknown = 0, + DidNotCrash = 1, + Crashed = 2, + } [System.Flags] public enum DeduplicateMode { @@ -202,6 +208,7 @@ namespace Sentry Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope); Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null); Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null); + Sentry.CrashedLastRun CrashedLastRun(); void EndSession(Sentry.SessionEndStatus status = 0); Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); @@ -822,6 +829,7 @@ namespace Sentry public static System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public static Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public static Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public static Sentry.CrashedLastRun CrashedLastRun() { } public static void EndSession(Sentry.SessionEndStatus status = 0) { } public static void Flush() { } public static void Flush(System.TimeSpan timeout) { } @@ -1323,6 +1331,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void Dispose() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } @@ -1369,6 +1378,7 @@ namespace Sentry.Extensibility public System.Threading.Tasks.Task ConfigureScopeAsync(System.Func configureScope) { } public Sentry.TransactionContext ContinueTrace(Sentry.SentryTraceHeader? traceHeader, Sentry.BaggageHeader? baggageHeader, string? name = null, string? operation = null) { } public Sentry.TransactionContext ContinueTrace(string? traceHeader, string? baggageHeader, string? name = null, string? operation = null) { } + public Sentry.CrashedLastRun CrashedLastRun() { } public void EndSession(Sentry.SessionEndStatus status = 0) { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } public Sentry.BaggageHeader? GetBaggage() { } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index d22326b119..b0c48b5d1c 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1706,6 +1706,73 @@ public void CaptureTransaction_Client_Gets_Hint() _fixture.Client.Received().CaptureTransaction(Arg.Any(), Arg.Any(), Arg.Any()); } + [Fact] + public void GetLastRunState_WithoutInit_ReturnsUnknown() + { + // Make sure SDK is closed + SentrySdk.Close(); + + // Act + var result = SentrySdk.CrashedLastRun(); + + // Assert + Assert.Equal(CrashedLastRun.Unknown, result); + } + + [Fact] + public void GetLastRunState_WhenCrashed_ReturnsCrashed() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + CrashedLastRun = () => true // Mock crashed state + }; + + // Act + SentrySdk.Init(options); + var result = SentrySdk.CrashedLastRun(); + + // Assert + Assert.Equal(CrashedLastRun.Crashed, result); + } + + [Fact] + public void GetLastRunState_WhenNotCrashed_ReturnsDidNotCrash() + { + // Arrange + var options = new SentryOptions() + { + Dsn = ValidDsn, + CrashedLastRun = () => false // Mock non-crashed state + }; + + // Act + SentrySdk.Init(options); + var result = SentrySdk.CrashedLastRun(); + + // Assert + Assert.Equal(CrashedLastRun.DidNotCrash, result); + } + + [Fact] + public void GetLastRunState_WithNullDelegate_ReturnsUnknown() + { + // Arrange + var options = new SentryOptions + { + Dsn = ValidDsn, + CrashedLastRun = null // Explicitly set to null + }; + + // Act + SentrySdk.Init(options); + var result = SentrySdk.CrashedLastRun(); + + // Assert + Assert.Equal(CrashedLastRun.Unknown, result); + } + [SkippableTheory] [InlineData(false)] [InlineData(true)]