diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b1df154..a5f998420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Features +- The trace used to connect errors on different layers of your game now gets regenerated every time the app gains focus again, or the active scene changes ([#2123](https://github.com/getsentry/sentry-unity/pull/2123)) - The SDK now links errors and events (managed and native errors) via `trace ID`. This allows you to correlate events captured from different layers of your game ([#1997](https://github.com/getsentry/sentry-unity/pull/1997), [#2089](https://github.com/getsentry/sentry-unity/pull/2089), [#2106](https://github.com/getsentry/sentry-unity/pull/2106)) - Drastically improved performance of scope sync when targeting Android ([#2107](https://github.com/getsentry/sentry-unity/pull/2107)) - The SDK now reports the game's name as part of the app context ([2083](https://github.com/getsentry/sentry-unity/pull/2083)) diff --git a/src/Sentry.Unity/Integrations/SessionIntegration.cs b/src/Sentry.Unity/Integrations/SessionIntegration.cs index d02d2c0c2..c966d2227 100644 --- a/src/Sentry.Unity/Integrations/SessionIntegration.cs +++ b/src/Sentry.Unity/Integrations/SessionIntegration.cs @@ -19,8 +19,6 @@ public void Register(IHub hub, SentryOptions options) return; } - options.DiagnosticLogger?.LogDebug("Registering Session integration."); - _sentryMonoBehaviour.ApplicationResuming += () => { options.DiagnosticLogger?.LogDebug("Resuming session."); diff --git a/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs b/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs new file mode 100644 index 000000000..89a98c768 --- /dev/null +++ b/src/Sentry.Unity/Integrations/TraceGenerationIntegration.cs @@ -0,0 +1,39 @@ +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Unity.Integrations; + +internal sealed class TraceGenerationIntegration : ISdkIntegration +{ + private readonly ISceneManager _sceneManager; + private readonly ISentryMonoBehaviour _sentryMonoBehaviour; + + public TraceGenerationIntegration(SentryMonoBehaviour sentryMonoBehaviour) : this(sentryMonoBehaviour, SceneManagerAdapter.Instance) + { } + + internal TraceGenerationIntegration(ISentryMonoBehaviour sentryMonoBehaviour, ISceneManager sceneManager) + { + _sceneManager = sceneManager; + _sentryMonoBehaviour = sentryMonoBehaviour; + } + + public void Register(IHub hub, SentryOptions options) + { + hub.ConfigureScope(UpdatePropagationContext); + + _sentryMonoBehaviour.ApplicationResuming += () => + { + options.DiagnosticLogger?.LogDebug("Application resumed. Creating new Trace."); + hub.ConfigureScope(UpdatePropagationContext); + }; + + _sceneManager.ActiveSceneChanged += (_, _) => + { + options.DiagnosticLogger?.LogDebug("Active Scene changed. Creating new Trace."); + hub.ConfigureScope(UpdatePropagationContext); + }; + } + + private static void UpdatePropagationContext(Scope scope) => + scope.SetPropagationContext(new SentryPropagationContext()); +} diff --git a/src/Sentry.Unity/SentryMonoBehaviour.cs b/src/Sentry.Unity/SentryMonoBehaviour.cs index f2ba0adf6..0945d3e72 100644 --- a/src/Sentry.Unity/SentryMonoBehaviour.cs +++ b/src/Sentry.Unity/SentryMonoBehaviour.cs @@ -4,11 +4,16 @@ namespace Sentry.Unity; +internal interface ISentryMonoBehaviour +{ + event Action? ApplicationResuming; +} + /// /// Singleton and DontDestroyOnLoad setup. /// [AddComponentMenu("")] // Hides it from being added as a component in the inspector -public partial class SentryMonoBehaviour : MonoBehaviour +public partial class SentryMonoBehaviour : MonoBehaviour, ISentryMonoBehaviour { private static SentryMonoBehaviour? _instance; public static SentryMonoBehaviour Instance diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 7cc9773f5..7be25356c 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -319,6 +319,7 @@ internal SentryUnityOptions(SentryMonoBehaviour behaviour, IApplication applicat this.AddIntegration(new UnityBeforeSceneLoadIntegration()); this.AddIntegration(new SceneManagerIntegration()); this.AddIntegration(new SessionIntegration(behaviour)); + this.AddIntegration(new TraceGenerationIntegration(behaviour)); this.AddExceptionFilter(new UnityBadGatewayExceptionFilter()); this.AddExceptionFilter(new UnityWebExceptionFilter()); diff --git a/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs new file mode 100644 index 000000000..751c35d5a --- /dev/null +++ b/test/Sentry.Unity.Tests/TraceGenerationIntegrationTests.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using NUnit.Framework; +using Sentry.Unity.Integrations; +using Sentry.Unity.Tests.SharedClasses; +using Sentry.Unity.Tests.Stubs; +using static Sentry.Unity.Tests.SceneManagerIntegrationTests; + +namespace Sentry.Unity.Tests; + +public class TraceGenerationIntegrationTests +{ + private class Fixture + { + public FakeSceneManager SceneManager { get; set; } = new(); + public TestSentryMonoBehaviour SentryMonoBehaviour { get; set; } = new(); + public TestHub TestHub { get; set; } = new(); + public TestLogger Logger { get; set; } = new(); + public SentryOptions SentryOptions { get; set; } + + public Fixture() => SentryOptions = new SentryOptions { DiagnosticLogger = Logger }; + + public TraceGenerationIntegration GetSut() => new(SentryMonoBehaviour, SceneManager); + } + + private readonly Fixture _fixture = new(); + + [SetUp] + public void SetUp() + { + _fixture.TestHub = new TestHub(); + SentrySdk.UseHub(_fixture.TestHub); + } + + [Test] + public void TraceGeneration_OnRegister_GeneratesInitialTrace() + { + // Arrange + var sut = _fixture.GetSut(); + + // Act + sut.Register(_fixture.TestHub, _fixture.SentryOptions); + + // Assert + var configureScope = _fixture.TestHub.ConfigureScopeCalls.Single(); + var scope = new Scope(_fixture.SentryOptions); + var initialPropagationContext = scope.PropagationContext; + configureScope(scope); + + Assert.AreNotEqual(initialPropagationContext, scope.PropagationContext); + } + + [Test] + public void TraceGeneration_OnApplicationResume_GeneratesNewTrace() + { + // Arrange + var sut = _fixture.GetSut(); + sut.Register(_fixture.TestHub, _fixture.SentryOptions); + var initialCallsCount = _fixture.TestHub.ConfigureScopeCalls.Count; + + // Act + _fixture.SentryMonoBehaviour.ResumeApplication(); + + // Assert + // Calling 'Register' already generated a trace, so we expect 1+1 calls to ConfigureScope + 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); + } + + [Test] + public void TraceGeneration_OnActiveSceneChange_GeneratesNewTrace() + { + // Arrange + var sut = _fixture.GetSut(); + sut.Register(_fixture.TestHub, _fixture.SentryOptions); + var initialCallsCount = _fixture.TestHub.ConfigureScopeCalls.Count; + + // Act + _fixture.SceneManager.OnActiveSceneChanged(new SceneAdapter("from scene name"), new SceneAdapter("to scene name")); + + // Assert + // Calling 'Register' already generated a trace, so we expect 1+1 calls to ConfigureScope + 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); + } + + internal class TestSentryMonoBehaviour : ISentryMonoBehaviour + { + public event Action? ApplicationResuming; + + public void ResumeApplication() => ApplicationResuming?.Invoke(); + } +}