-
-
Couldn't load subscription status.
- Fork 225
feat: Cache Unhandled session instead of sending it immediately
#4653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/session-type-unhandled
Are you sure you want to change the base?
Changes from 6 commits
d9c2d73
413a9e8
660714e
6508e5e
1348053
0502cd7
b94c400
225f67a
f06513d
7527bdd
01d6bb8
a7ff327
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,13 @@ public class SentrySession : ISentrySession | |
| // Start at -1 so that the first increment puts it at 0 | ||
| private int _sequenceNumber = -1; | ||
|
|
||
| private bool _hasPendingUnhandledException; | ||
|
|
||
| /// <summary> | ||
| /// Gets whether this session has an unhandled exception that hasn't been finalized yet. | ||
| /// </summary> | ||
| internal bool HasPendingUnhandledException => _hasPendingUnhandledException; | ||
|
|
||
| internal SentrySession( | ||
| SentryId id, | ||
| string? distinctId, | ||
|
|
@@ -74,6 +81,16 @@ public SentrySession(string? distinctId, string release, string? environment) | |
| /// </summary> | ||
| public void ReportError() => Interlocked.Increment(ref _errorCount); | ||
|
|
||
| /// <summary> | ||
| /// Marks the session as having an unhandled exception without ending it. | ||
| /// This allows the session to continue and potentially escalate to Crashed if the app crashes. | ||
| /// </summary> | ||
| internal void MarkUnhandledException() | ||
| { | ||
| _hasPendingUnhandledException = true; | ||
| ReportError(); | ||
| } | ||
|
||
|
|
||
| internal SessionUpdate CreateUpdate( | ||
| bool isInitial, | ||
| DateTimeOffset timestamp, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -523,6 +523,184 @@ | |
| TryRecoverPersistedSessionWithExceptionOnLastRun(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MarkSessionAsUnhandled_ActiveSessionExists_MarksSessionAndPersists() | ||
| { | ||
| // Arrange | ||
| var sut = _fixture.GetSut(); | ||
| sut.StartSession(); | ||
| var session = sut.CurrentSession; | ||
|
|
||
| // Act | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Assert | ||
| session.Should().NotBeNull(); | ||
| session!.HasPendingUnhandledException.Should().BeTrue(); | ||
| session.ErrorCount.Should().Be(1); | ||
|
|
||
| // Session should still be active (not ended) | ||
| sut.CurrentSession.Should().BeSameAs(session); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void MarkSessionAsUnhandled_NoActiveSession_LogsDebug() | ||
| { | ||
| // Arrange | ||
| var sut = _fixture.GetSut(); | ||
|
|
||
| // Act | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Assert | ||
| _fixture.Logger.Entries.Should().Contain(e => | ||
|
Check failure on line 556 in test/Sentry.Tests/GlobalSessionManagerTests.cs
|
||
| e.Message == "No active session to mark as unhandled." && | ||
| e.Level == SentryLevel.Debug); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Session Logging Mismatch Causes Test FailureThe Additional Locations (1) |
||
| } | ||
|
|
||
| [Fact] | ||
| public void MarkSessionAsUnhandled_MultipleUnhandledExceptions_OnlyCountsFirstError() | ||
| { | ||
| // Arrange | ||
| var sut = _fixture.GetSut(); | ||
| sut.StartSession(); | ||
| var session = sut.CurrentSession; | ||
|
|
||
| // Act | ||
| sut.MarkSessionAsUnhandled(); | ||
| sut.MarkSessionAsUnhandled(); | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Assert | ||
| session!.ErrorCount.Should().Be(1); | ||
|
Check failure on line 575 in test/Sentry.Tests/GlobalSessionManagerTests.cs
|
||
| } | ||
|
|
||
| [Fact] | ||
| public void TryRecoverPersistedSession_WithPendingUnhandledAndNoCrash_EndsAsUnhandled() | ||
| { | ||
| // Arrange | ||
| _fixture.Options.CrashedLastRun = () => false; | ||
| _fixture.PersistedSessionProvider = _ => new PersistedSessionUpdate( | ||
| AnySessionUpdate(), | ||
| pauseTimestamp: null, | ||
| pendingUnhandled: true); | ||
|
|
||
| var sut = _fixture.GetSut(); | ||
|
|
||
| // Act | ||
| var persistedSessionUpdate = sut.TryRecoverPersistedSession(); | ||
|
|
||
| // Assert | ||
| persistedSessionUpdate.Should().NotBeNull(); | ||
| persistedSessionUpdate!.EndStatus.Should().Be(SessionEndStatus.Unhandled); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryRecoverPersistedSession_WithPendingUnhandledAndCrash_EscalatesToCrashed() | ||
| { | ||
| // Arrange | ||
| _fixture.Options.CrashedLastRun = () => true; | ||
| _fixture.PersistedSessionProvider = _ => new PersistedSessionUpdate( | ||
| AnySessionUpdate(), | ||
| pauseTimestamp: null, | ||
| pendingUnhandled: true); | ||
|
|
||
| var sut = _fixture.GetSut(); | ||
|
|
||
| // Act | ||
| var persistedSessionUpdate = sut.TryRecoverPersistedSession(); | ||
|
|
||
| // Assert | ||
| persistedSessionUpdate.Should().NotBeNull(); | ||
| persistedSessionUpdate!.EndStatus.Should().Be(SessionEndStatus.Crashed); | ||
|
|
||
| _fixture.Logger.Entries.Should().Contain(e => | ||
|
Check failure on line 617 in test/Sentry.Tests/GlobalSessionManagerTests.cs
|
||
| e.Message.Contains("PendingUnhandled: True") && | ||
| e.Level == SentryLevel.Info); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void TryRecoverPersistedSession_WithPendingUnhandledAndPauseTimestamp_EscalatesToCrashedIfCrashed() | ||
| { | ||
| // Arrange - Session was paused AND had pending unhandled, then crashed | ||
| _fixture.Options.CrashedLastRun = () => true; | ||
| var pausedTimestamp = DateTimeOffset.Now; | ||
| _fixture.PersistedSessionProvider = _ => new PersistedSessionUpdate( | ||
| AnySessionUpdate(), | ||
| pausedTimestamp, | ||
| pendingUnhandled: true); | ||
|
|
||
| var sut = _fixture.GetSut(); | ||
|
|
||
| // Act | ||
| var persistedSessionUpdate = sut.TryRecoverPersistedSession(); | ||
|
|
||
| // Assert | ||
| // Crash takes priority over all other end statuses | ||
| persistedSessionUpdate.Should().NotBeNull(); | ||
| persistedSessionUpdate!.EndStatus.Should().Be(SessionEndStatus.Crashed); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void EndSession_WithPendingUnhandledException_PreservesUnhandledStatus() | ||
| { | ||
| // Arrange | ||
| var sut = _fixture.GetSut(); | ||
| sut.StartSession(); | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Act - Try to end normally with Exited status | ||
| var sessionUpdate = sut.EndSession(SessionEndStatus.Exited); | ||
|
|
||
| // Assert - Should be overridden to Unhandled | ||
| sessionUpdate.Should().NotBeNull(); | ||
| sessionUpdate!.EndStatus.Should().Be(SessionEndStatus.Unhandled); | ||
| sessionUpdate.ErrorCount.Should().Be(1); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void EndSession_WithPendingUnhandledAndCrashedStatus_UsesCrashedStatus() | ||
| { | ||
| // Arrange | ||
| var sut = _fixture.GetSut(); | ||
| sut.StartSession(); | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Act - Explicitly end with Crashed status | ||
| var sessionUpdate = sut.EndSession(SessionEndStatus.Crashed); | ||
|
|
||
| // Assert - Crashed status takes priority | ||
| sessionUpdate.Should().NotBeNull(); | ||
| sessionUpdate!.EndStatus.Should().Be(SessionEndStatus.Crashed); | ||
| sessionUpdate.ErrorCount.Should().Be(1); | ||
|
Check failure on line 675 in test/Sentry.Tests/GlobalSessionManagerTests.cs
|
||
| } | ||
|
|
||
| [Fact] | ||
| public void SessionEscalation_CompleteFlow_UnhandledThenCrash() | ||
| { | ||
| // Arrange - Simulate complete flow | ||
| var sut = _fixture.GetSut(); | ||
| sut.StartSession(); | ||
| var originalSessionId = sut.CurrentSession!.Id; | ||
|
|
||
| // Act 1: Mark as unhandled (game encounters exception but continues) | ||
| sut.MarkSessionAsUnhandled(); | ||
|
|
||
| // Assert: Session still active with pending flag | ||
| sut.CurrentSession.Should().NotBeNull(); | ||
| sut.CurrentSession!.Id.Should().Be(originalSessionId); | ||
| sut.CurrentSession.HasPendingUnhandledException.Should().BeTrue(); | ||
|
|
||
| // Act 2: Recover on next launch with crash detected | ||
| _fixture.Options.CrashedLastRun = () => true; | ||
| var recovered = sut.TryRecoverPersistedSession(); | ||
|
|
||
| // Assert: Session escalated from Unhandled to Crashed | ||
| recovered.Should().NotBeNull(); | ||
| recovered!.EndStatus.Should().Be(SessionEndStatus.Crashed); | ||
| recovered.Id.Should().Be(originalSessionId); | ||
| } | ||
|
|
||
| // A session update (of which the state doesn't matter for the test): | ||
| private static SessionUpdate AnySessionUpdate() | ||
| => new( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFrameworks>$(CurrentTfms)</TargetFrameworks> | ||
| <TargetFrameworks>$(CurrentTfms);net48</TargetFrameworks> | ||
|
||
| <TargetFrameworks Condition="'$(NO_ANDROID)' == ''">$(TargetFrameworks);$(LatestAndroidTfm);$(PreviousAndroidTfm)</TargetFrameworks> | ||
| <TargetFrameworks Condition="'$(NO_IOS)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);$(LatestIosTfm);$(PreviousIosTfm)</TargetFrameworks> | ||
| <TargetFrameworks Condition="'$(NO_MACCATALYST)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);$(LatestMacCatalystTfm);$(PreviousMacCatalystTfm)</TargetFrameworks> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.