diff --git a/CHANGELOG.md b/CHANGELOG.md index a89e9fdcd..57ff64e77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - `sentry-native` is now built on Ubuntu 22.04 instead of Ubuntu 20.04, which reached EOL in May 2025. If you are running you game on a server on Ubuntu 20.04, you should update the OS before upgrading to this SDK version. ([#2355](https://github.com/getsentry/sentry-unity/pull/2355)) +### Behavioural Changes + +- The SDK no longer refreshes the trace ID when the app loses and regains focus. This means that the trace ID persists from game start to game end. The SDK now also automatically adds breadcrumbs on those lifecycle events. ([#2374](https://github.com/getsentry/sentry-unity/pull/2374)) + ### Features - Added support for Structured Logging. The `SentrySdk.Logger` API is now exposed for Unity users, enabling structured log capture. The SDK can also automatically capture and send Debug logs based on the options configured. ([#2368](https://github.com/getsentry/sentry-unity/pull/2368)) diff --git a/src/Sentry.Unity/Integrations/LifeCycleIntegration.cs b/src/Sentry.Unity/Integrations/LifeCycleIntegration.cs new file mode 100644 index 000000000..7a315221c --- /dev/null +++ b/src/Sentry.Unity/Integrations/LifeCycleIntegration.cs @@ -0,0 +1,81 @@ +using System; +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Unity.Integrations; + +internal class LifeCycleIntegration : ISdkIntegration +{ + private IHub? _hub; + private SentryUnityOptions _options = null!; // Set during register + + private readonly SentryMonoBehaviour _sentryMonoBehaviour; + private readonly IApplication _application; + + public LifeCycleIntegration(SentryMonoBehaviour sentryMonoBehaviour, IApplication? application = null) + { + _application = application ?? ApplicationAdapter.Instance; + _sentryMonoBehaviour = sentryMonoBehaviour; + } + + public void Register(IHub hub, SentryOptions sentryOptions) + { + _hub = hub; + // This should never happen, but if it does... + _options = sentryOptions as SentryUnityOptions ?? throw new ArgumentException("Options is not of type 'SentryUnityOptions'."); + + if (!_options.AutoSessionTracking) + { + return; + } + + _sentryMonoBehaviour.ApplicationResuming += () => + { + if (!hub.IsEnabled) + { + return; + } + + hub.AddBreadcrumb(message: "App regained focus.", category: "app.lifecycle"); + + _options.DiagnosticLogger?.LogDebug("Resuming session."); + hub.ResumeSession(); + }; + _sentryMonoBehaviour.ApplicationPausing += () => + { + if (!hub.IsEnabled) + { + return; + } + + hub.AddBreadcrumb(message: "App lost focus.", category: "app.lifecycle"); + + _options.DiagnosticLogger?.LogDebug("Pausing session."); + hub.PauseSession(); + }; + + _application.Quitting += OnQuitting; + } + + private void OnQuitting() + { + // Platform-specific behavior notes: + // - iOS: Applications are usually suspended and do not quit. If `Exit on Suspend` is enabled in Player Settings, + // the application will be terminated on suspend instead of calling this method. In that case, + // `OnApplicationPause` will be called instead. + // - Windows Store Apps/Windows Phone 8.1: No application quit event exists. Use OnApplicationFocus instead. + // - WebGL: OnApplicationQuit cannot be implemented due to browser tab closing behavior. + + // Session handling on shutdown: + // This method is invoked even when an uncaught exception occurs (including crashes in native layers). + // We pause the session here rather than ending it to ensure the .NET SDK can properly detect crashes + // on the next startup (via the CrashedLastRun callback). The session will then be closed with the + // correct timestamp during initialization. + if (_options.AutoSessionTracking) + { + _hub?.PauseSession(); + } + + _hub?.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); + } +} diff --git a/src/Sentry.Unity/Integrations/SessionIntegration.cs b/src/Sentry.Unity/Integrations/SessionIntegration.cs deleted file mode 100644 index c966d2227..000000000 --- a/src/Sentry.Unity/Integrations/SessionIntegration.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Sentry.Extensibility; -using Sentry.Integrations; - -namespace Sentry.Unity.Integrations; - -internal class SessionIntegration : ISdkIntegration -{ - private readonly SentryMonoBehaviour _sentryMonoBehaviour; - - public SessionIntegration(SentryMonoBehaviour sentryMonoBehaviour) - { - _sentryMonoBehaviour = sentryMonoBehaviour; - } - - public void Register(IHub hub, SentryOptions options) - { - if (!options.AutoSessionTracking) - { - return; - } - - _sentryMonoBehaviour.ApplicationResuming += () => - { - options.DiagnosticLogger?.LogDebug("Resuming session."); - hub.ResumeSession(); - }; - _sentryMonoBehaviour.ApplicationPausing += () => - { - options.DiagnosticLogger?.LogDebug("Pausing session."); - hub.PauseSession(); - }; - } -} diff --git a/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs b/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs index fa9c12b4d..42fc56c73 100644 --- a/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs +++ b/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs @@ -27,12 +27,6 @@ public void Register(IHub hub, SentryOptions options) return; } - _sentryMonoBehaviour.ApplicationResuming += () => - { - options.DiagnosticLogger?.LogDebug("Game resuming. Creating new Trace."); - hub.ConfigureScope(scope => scope.SetPropagationContext(new SentryPropagationContext())); - }; - var isTracingEnabled = unityOptions.TracesSampleRate > 0.0f; // Create initial trace context if tracing is disabled or startup tracing is disabled diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index d869a4ceb..b2b8f74cd 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -12,21 +12,14 @@ namespace Sentry.Unity.Integrations; /// internal sealed class UnityLogHandlerIntegration : ISdkIntegration, ILogHandler { - private readonly IApplication _application; private readonly Func? _loggerFactory; private IHub? _hub; private SentryUnityOptions _options = null!; // Set during register private ILogHandler _unityLogHandler = null!; // Set during register private SentryStructuredLogger _structuredLogger = null!; // Set during register - public UnityLogHandlerIntegration(IApplication? application = null) - { - _application = application ?? ApplicationAdapter.Instance; - } - // For testing: allows injecting a custom logger factory - internal UnityLogHandlerIntegration(IApplication? application, Func loggerFactory) - : this(application) + internal UnityLogHandlerIntegration(Func? loggerFactory = null) { _loggerFactory = loggerFactory; } @@ -48,8 +41,6 @@ public void Register(IHub hub, SentryOptions sentryOptions) _unityLogHandler = Debug.unityLogger.logHandler; Debug.unityLogger.logHandler = this; - - _application.Quitting += OnQuitting; } public void LogException(Exception exception, UnityEngine.Object context) @@ -142,23 +133,4 @@ private void ProcessStructuredLog(LogType logType, string format, params object[ break; } } - - private void OnQuitting() - { - _options.DiagnosticLogger?.LogInfo("OnQuitting was invoked. Unhooking log callback and pausing session."); - - // Note: iOS applications are usually suspended and do not quit. You should tick "Exit on Suspend" in Player settings for iOS builds to cause the game to quit and not suspend, otherwise you may not see this call. - // If "Exit on Suspend" is not ticked then you will see calls to OnApplicationPause instead. - // Note: On Windows Store Apps and Windows Phone 8.1 there is no application quit event. Consider using OnApplicationFocus event when focusStatus equals false. - // Note: On WebGL it is not possible to implement OnApplicationQuit due to nature of the browser tabs closing. - - // 'OnQuitting' is invoked even when an uncaught exception happens in the ART. To make sure the .NET - // SDK checks with the native layer on restart if the previous run crashed (through the CrashedLastRun callback) - // we'll just pause sessions on shutdown. On restart they can be closed with the right timestamp and as 'exited'. - if (_options.AutoSessionTracking) - { - _hub?.PauseSession(); - } - _hub?.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); - } } diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 2555296a4..5dfcaa994 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -358,7 +358,7 @@ internal SentryUnityOptions(IApplication? application = null, AddIntegration(new UnityBeforeSceneLoadIntegration()); AddIntegration(new SceneManagerIntegration()); AddIntegration(new SceneManagerTracingIntegration()); - AddIntegration(new SessionIntegration(behaviour)); + AddIntegration(new LifeCycleIntegration(behaviour)); AddIntegration(new TraceGenerationIntegration(behaviour)); AddExceptionFilter(new UnityBadGatewayExceptionFilter()); diff --git a/test/Sentry.Unity.Tests/SessionIntegrationTests.cs b/test/Sentry.Unity.Tests/LifeCycleIntegrationTests.cs similarity index 96% rename from test/Sentry.Unity.Tests/SessionIntegrationTests.cs rename to test/Sentry.Unity.Tests/LifeCycleIntegrationTests.cs index d53f81698..598715499 100644 --- a/test/Sentry.Unity.Tests/SessionIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/LifeCycleIntegrationTests.cs @@ -6,7 +6,7 @@ namespace Sentry.Unity.Tests; -public class SessionIntegrationTests +public class LifeCycleIntegrationTests { [UnityTest] public IEnumerator SessionIntegration_Init_SentryMonoBehaviourCreated() diff --git a/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs index 9e53f0393..95547f515 100644 --- a/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs @@ -68,27 +68,6 @@ public void Register_TracingEnabledAndAutoStartupTracesEnabled_DoesNotGenerateIn Assert.IsEmpty(_fixture.TestHub.ConfigureScopeCalls); } - [Test] - public void ApplicationResuming_WhenCalled_GeneratesNewTrace() - { - // Arrange - var sut = _fixture.GetSut(); - sut.Register(_fixture.TestHub, _fixture.SentryOptions); - var initialCallsCount = _fixture.TestHub.ConfigureScopeCalls.Count; - - // Act - _fixture.SentryMonoBehaviour.ResumeApplication(); - - // Assert - Assert.AreEqual(initialCallsCount + 1, _fixture.TestHub.ConfigureScopeCalls.Count); - var configureScope = _fixture.TestHub.ConfigureScopeCalls.Last(); - var scope = new Scope(_fixture.SentryOptions); - var initialPropagationContext = scope.PropagationContext; - configureScope(scope); - - Assert.AreNotEqual(initialPropagationContext, scope.PropagationContext); - } - [TestCase(0.0f, false)] [TestCase(0.0f, true)] [TestCase(1.0f, false)] diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index 7edff354c..2f79a70b3 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -21,10 +21,9 @@ private class Fixture public UnityLogHandlerIntegration GetSut() { - var application = new TestApplication(); var integration = StructuredLogger != null - ? new UnityLogHandlerIntegration(application, () => StructuredLogger) - : new UnityLogHandlerIntegration(application, () => DisabledSentryStructuredLogger.Instance); + ? new UnityLogHandlerIntegration(() => StructuredLogger) + : new UnityLogHandlerIntegration(() => DisabledSentryStructuredLogger.Instance); integration.Register(Hub, SentryOptions); return integration; }