From e75f147f14a110b513fe1549b1a7bf0e1e2ef27c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 22 Oct 2025 14:24:55 +0200 Subject: [PATCH 01/18] Unseal --- src/sentry-dotnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry-dotnet b/src/sentry-dotnet index 080493bf5..b887fb347 160000 --- a/src/sentry-dotnet +++ b/src/sentry-dotnet @@ -1 +1 @@ -Subproject commit 080493bf59dc5fbf49bcae0a4d203bce0c86447a +Subproject commit b887fb3478beb844731504819200c09c8c6ae04a From 7f88a98e59384cfd3d77c3947fc05d6227510985 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 22 Oct 2025 16:50:18 +0200 Subject: [PATCH 02/18] Added structured logging capture to --- .../Resources/Sentry/SentryOptions.asset | 10 +- .../ConfigurationWindow/LoggingTab.cs | 136 ++++++++++------ .../UnityApplicationLoggingIntegration.cs | 149 ++++++++++++------ .../ScriptableSentryUnityOptions.cs | 19 +++ src/Sentry.Unity/SentryUnityOptions.cs | 29 ++++ 5 files changed, 247 insertions(+), 96 deletions(-) diff --git a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset index 542d04e04..e04678b85 100644 --- a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset +++ b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset @@ -27,7 +27,7 @@ MonoBehaviour: k__BackingField: 30000 k__BackingField: k__BackingField: - k__BackingField: 1 + k__BackingField: 0 k__BackingField: 1 k__BackingField: 1 k__BackingField: 75 @@ -35,11 +35,17 @@ MonoBehaviour: k__BackingField: 100 k__BackingField: 20 k__BackingField: 10 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 - k__BackingField: 1 k__BackingField: 1 k__BackingField: 100 k__BackingField: 1 diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs index da13dd041..12cc02c7f 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs @@ -9,31 +9,35 @@ internal static class LoggingTab internal static void Display(ScriptableSentryUnityOptions options) { { - options.EnableLogDebouncing = EditorGUILayout.BeginToggleGroup( - new GUIContent("Enable Log Debouncing", "The SDK debounces log messages of the " + - "same type if they are more frequent than once per second."), - options.EnableLogDebouncing); + GUILayout.Label("Structured Logging - Experimental", EditorStyles.boldLabel); + + options.EnableStructuredLogging = EditorGUILayout.BeginToggleGroup( + new GUIContent("Send Logs for:", "Enables the SDK to forward log messages to Sentry " + + "based on the log level."), + options.EnableStructuredLogging); EditorGUI.indentLevel++; - options.DebounceTimeLog = EditorGUILayout.IntField( - new GUIContent("Log Debounce [ms]", "The time that has to pass between events of " + - "LogType.Log before the SDK sends it again."), - options.DebounceTimeLog); - options.DebounceTimeLog = Math.Max(0, options.DebounceTimeLog); - - options.DebounceTimeWarning = EditorGUILayout.IntField( - new GUIContent("Warning Debounce [ms]", "The time that has to pass between events of " + - "LogType.Warning before the SDK sends it again."), - options.DebounceTimeWarning); - options.DebounceTimeWarning = Math.Max(0, options.DebounceTimeWarning); - - options.DebounceTimeError = EditorGUILayout.IntField( - new GUIContent("Error Debounce [ms]", "The time that has to pass between events of " + - "LogType.Assert, LogType.Exception and LogType.Error before " + - "the SDK sends it again."), - options.DebounceTimeError); - options.DebounceTimeError = Math.Max(0, options.DebounceTimeError); + options.OnDebugLog = EditorGUILayout.Toggle( + new GUIContent("Debug.Log", + "Whether the SDK should forward Debug.Log messages to Sentry structured logging"), + options.OnDebugLog); + options.OnDebugLogWarning = EditorGUILayout.Toggle( + new GUIContent("Debug.LogWarning", + "Whether the SDK should forward Debug.LogWarning messages to Sentry structured logging"), + options.OnDebugLogWarning); + options.OnDebugLogAssertion = EditorGUILayout.Toggle( + new GUIContent("Debug.LogAssertion", + "Whether the SDK should forward Debug.LogAssertion messages to Sentry structured logging"), + options.OnDebugLogAssertion); + options.OnDebugLogError = EditorGUILayout.Toggle( + new GUIContent("Debug.LogError", + "Whether the SDK should forward Debug.LogError messages to Sentry structured logging"), + options.OnDebugLogError); + options.OnDebugLogException = EditorGUILayout.Toggle( + new GUIContent("Debug.LogException", + "Whether the SDK should forward Debug.LogException messages to Sentry structured logging"), + options.OnDebugLogException); EditorGUI.indentLevel--; EditorGUILayout.EndToggleGroup(); @@ -44,42 +48,45 @@ internal static void Display(ScriptableSentryUnityOptions options) EditorGUILayout.Space(); { - GUILayout.Label("Automatically capture and send events for:", EditorStyles.boldLabel); - EditorGUI.indentLevel++; + GUILayout.Label("Breadcrumbs", EditorStyles.boldLabel); - options.CaptureLogErrorEvents = EditorGUILayout.Toggle( - new GUIContent("Debug.LogError", "Whether the SDK automatically captures events for 'Debug.LogError'."), - options.CaptureLogErrorEvents); + if (options.EnableStructuredLogging) + { + options.AttachBreadcrumbsToEvents = EditorGUILayout.BeginToggleGroup( + new GUIContent("Attach logs as breadcrumbs in addition to sending them as structured logs", "Whether the SDK should attach breadcrumbs to events in addition to structured logging."), + options.AttachBreadcrumbsToEvents); - EditorGUILayout.Space(); - GUILayout.Label("Automatically add breadcrumbs for:", EditorStyles.boldLabel); + GUILayout.Label("Note: With sending structured logs enabled you have to opt-into adding breadcrumbs to events.", EditorStyles.boldLabel); + } + + EditorGUI.indentLevel++; options.BreadcrumbsForLogs = EditorGUILayout.Toggle( new GUIContent("Debug.Log", "Whether the SDK automatically adds breadcrumbs 'Debug.Log'."), options.BreadcrumbsForLogs); options.BreadcrumbsForWarnings = EditorGUILayout.Toggle( - new GUIContent("Debug.Warning", "Whether the SDK automatically adds breadcrumbs for 'Debug.LogWarning'."), + new GUIContent("Debug.LogWarning", "Whether the SDK automatically adds breadcrumbs for 'Debug.LogWarning'."), options.BreadcrumbsForWarnings); options.BreadcrumbsForAsserts = EditorGUILayout.Toggle( - new GUIContent("Debug.Assert", "Whether the SDK automatically adds breadcrumbs for 'Debug.Assert'."), + new GUIContent("Debug.LogAssertion", "Whether the SDK automatically adds breadcrumbs for 'Debug.LogAssertion'."), options.BreadcrumbsForAsserts); options.BreadcrumbsForErrors = EditorGUILayout.Toggle( - new GUIContent("Debug.Error", "Whether the SDK automatically adds breadcrumbs for 'Debug.LogError'."), + new GUIContent("Debug.LogError", "Whether the SDK automatically adds breadcrumbs for 'Debug.LogError'."), options.BreadcrumbsForErrors); - EditorGUI.indentLevel--; - } - - EditorGUILayout.Space(); - EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); - EditorGUILayout.Space(); + EditorGUILayout.Space(); - { options.MaxBreadcrumbs = EditorGUILayout.IntField( new GUIContent("Max Breadcrumbs", "Maximum number of breadcrumbs that get captured." + "\nDefault: 100"), options.MaxBreadcrumbs); options.MaxBreadcrumbs = Math.Max(0, options.MaxBreadcrumbs); + + EditorGUI.indentLevel--; + if (options.EnableStructuredLogging) + { + EditorGUILayout.EndToggleGroup(); + } } EditorGUILayout.Space(); @@ -87,22 +94,63 @@ internal static void Display(ScriptableSentryUnityOptions options) EditorGUILayout.Space(); { - GUILayout.Label("Attach the stack trace when capturing log messages. NOTE: These will not contain line numbers.", EditorStyles.boldLabel); + GUILayout.Label("CaptureMessage Settings", EditorStyles.boldLabel); EditorGUI.indentLevel++; + options.CaptureLogErrorEvents = EditorGUILayout.Toggle( + new GUIContent("Capture LogError", "Whether the SDK automatically captures events for 'Debug.LogError'."), + options.CaptureLogErrorEvents); + options.AttachStacktrace = EditorGUILayout.Toggle( - new GUIContent("Attach Stack Trace", "Whether to include a stack trace for non " + - "error events like logs. Even when Unity didn't include and no " + - "exception was thrown. Refer to AttachStacktrace on sentry docs."), + new GUIContent("Attach Stack Trace", "Whether the SDK should include a stack trace for CaptureMessage " + + "events. Refer to AttachStacktrace on sentry docs."), options.AttachStacktrace); - EditorGUI.indentLevel--; + GUILayout.Label("Note: The stack trace quality will depend on the IL2CPP line number setting and might not contain line numbers.", EditorStyles.boldLabel); + // Enhanced not supported on IL2CPP so not displaying this for the time being: // Options.StackTraceMode = (StackTraceMode) EditorGUILayout.EnumPopup( // new GUIContent("Stacktrace Mode", "Enhanced is the default." + // "\n - Enhanced: Include async, return type, args,..." + // "\n - Original - Default .NET stack trace format."), // Options.StackTraceMode); + + EditorGUI.indentLevel--; + } + + EditorGUILayout.Space(); + EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1), Color.gray); + EditorGUILayout.Space(); + + { + options.EnableLogDebouncing = EditorGUILayout.BeginToggleGroup( + new GUIContent("Enable Log Debouncing", "The SDK debounces log messages of the " + + "same type if they are more frequent than once per second."), + options.EnableLogDebouncing); + + EditorGUI.indentLevel++; + + options.DebounceTimeLog = EditorGUILayout.IntField( + new GUIContent("Log Debounce [ms]", "The time that has to pass between events of " + + "LogType.Log before the SDK sends it again."), + options.DebounceTimeLog); + options.DebounceTimeLog = Math.Max(0, options.DebounceTimeLog); + + options.DebounceTimeWarning = EditorGUILayout.IntField( + new GUIContent("Warning Debounce [ms]", "The time that has to pass between events of " + + "LogType.Warning before the SDK sends it again."), + options.DebounceTimeWarning); + options.DebounceTimeWarning = Math.Max(0, options.DebounceTimeWarning); + + options.DebounceTimeError = EditorGUILayout.IntField( + new GUIContent("Error Debounce [ms]", "The time that has to pass between events of " + + "LogType.Assert, LogType.Exception and LogType.Error before " + + "the SDK sends it again."), + options.DebounceTimeError); + options.DebounceTimeError = Math.Max(0, options.DebounceTimeError); + + EditorGUI.indentLevel--; + EditorGUILayout.EndToggleGroup(); } } } diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index 52143fe65..479f1595f 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -1,3 +1,4 @@ +using System; using Sentry.Integrations; using UnityEngine; @@ -12,12 +13,13 @@ internal class UnityApplicationLoggingIntegration : ISdkIntegration { private readonly IApplication _application; private readonly bool _captureExceptions; - private ErrorTimeDebounce? _errorTimeDebounce; - private LogTimeDebounce? _logTimeDebounce; - private WarningTimeDebounce? _warningTimeDebounce; + + private ErrorTimeDebounce _errorTimeDebounce = null!; // Set in Register + private LogTimeDebounce _logTimeDebounce = null!; // Set in Register + private WarningTimeDebounce _warningTimeDebounce = null!; // Set in Register private IHub? _hub; - private SentryUnityOptions? _options; + private SentryUnityOptions _options = null!; // Set in Register internal UnityApplicationLoggingIntegration(bool captureExceptions = false, IApplication? application = null) { @@ -28,11 +30,8 @@ internal UnityApplicationLoggingIntegration(bool captureExceptions = false, IApp public void Register(IHub hub, SentryOptions sentryOptions) { _hub = hub; - _options = sentryOptions as SentryUnityOptions; - if (_options is null) - { - return; - } + // This should never throw + _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options passed is not of type SentryUnityOptions"); _logTimeDebounce = new LogTimeDebounce(_options.DebounceTimeLog); _warningTimeDebounce = new WarningTimeDebounce(_options.DebounceTimeWarning); @@ -49,72 +48,122 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo return; } - // We're not capturing or creating breadcrumbs from SDK logs - if (message.StartsWith(UnityLogger.LogTag)) + // We're not capturing the SDKs own logs + if (message.StartsWith(UnityLogger.LogTag) || IsGettingDebounced(logType)) { return; } - // LogType.Exception are getting handled by the UnityLogHandlerIntegration - // Unless we're configured to handle them - i.e. WebGL - if (logType is LogType.Exception && !_captureExceptions) + ProcessStructuredLog(message, logType); + ProcessException(message, stacktrace, logType); + ProcessError(message, stacktrace, logType); + ProcessBreadcrumbs(message, logType); + } + + private bool IsGettingDebounced(LogType logType) + { + if (_options?.EnableLogDebouncing is not true) { - return; + return false; } - if (_options?.EnableLogDebouncing is true) + var debounced = logType switch { - var debounced = logType switch - { - LogType.Exception => _errorTimeDebounce?.Debounced(), - LogType.Error or LogType.Assert => _errorTimeDebounce?.Debounced(), - LogType.Log => _logTimeDebounce?.Debounced(), - LogType.Warning => _warningTimeDebounce?.Debounced(), - _ => true - }; - - if (debounced is not true) - { - return; - } + LogType.Exception => _errorTimeDebounce.Debounced(), + LogType.Error or LogType.Assert => _errorTimeDebounce.Debounced(), + LogType.Log => _logTimeDebounce.Debounced(), + LogType.Warning => _warningTimeDebounce.Debounced(), + _ => true + }; + + return debounced; + } + + private void ProcessStructuredLog(string message, LogType logType) + { + switch (logType) + { + case LogType.Log: + if (_options.Experimental.OnDebugLog) + { + Sentry.SentrySdk.Logger.LogInfo(message); + } + break; + case LogType.Warning: + if (_options.Experimental.OnDebugLogWarning) + { + Sentry.SentrySdk.Logger.LogWarning(message); + } + break; + case LogType.Assert: + if (_options.Experimental.OnDebugLogAssertion) + { + Sentry.SentrySdk.Logger.LogError(message); + } + break; + case LogType.Error: + if (_options.Experimental.OnDebugLogError) + { + Sentry.SentrySdk.Logger.LogError(message); + } + break; + case LogType.Exception: + if (_options.Experimental.OnDebugLogException) + { + Sentry.SentrySdk.Logger.LogError(message); + } + break; } + } - if (logType is LogType.Exception) + private void ProcessException(string message, string stacktrace, LogType logType) + { + // LogType.Exception is getting handled by the `UnityLogHandlerIntegration` + // UNLESS we're configured to handle them - i.e. WebGL + if (logType is LogType.Exception && _captureExceptions) { var ule = new UnityErrorLogException(message, stacktrace, _options); - _hub.CaptureException(ule); + _hub?.CaptureException(ule); + } + } - // We don't capture breadcrumbs for exceptions - the .NET SDK handles this + private void ProcessError(string message, string stacktrace, LogType logType) + { + if (logType is not LogType.Error || !_options.CaptureLogErrorEvents) + { return; } - if (logType is LogType.Error && _options?.CaptureLogErrorEvents is true) + if (_options.AttachStacktrace && !string.IsNullOrEmpty(stacktrace)) { - if (_options?.AttachStacktrace is true && !string.IsNullOrEmpty(stacktrace)) - { - var ule = new UnityErrorLogException(message, stacktrace, _options); - var sentryEvent = new SentryEvent(ule) { Level = SentryLevel.Error }; - - _hub.CaptureEvent(sentryEvent); - } - else - { - _hub.CaptureMessage(message, level: SentryLevel.Error); - } - } + var ule = new UnityErrorLogException(message, stacktrace, _options); + var sentryEvent = new SentryEvent(ule) { Level = SentryLevel.Error }; - // Capture so the next event includes this error as breadcrumb - if (_options?.AddBreadcrumbsForLogType[logType] is true) + _hub?.CaptureEvent(sentryEvent); + } + else { - _hub.AddBreadcrumb(message: message, category: "unity.logger", level: ToBreadcrumbLevel(logType)); + _hub?.CaptureMessage(message, level: SentryLevel.Error); } } - private void OnQuitting() + private void ProcessBreadcrumbs(string message, LogType logType) { - _application.LogMessageReceived -= OnLogMessageReceived; + if (logType is LogType.Exception) + { + // Capturing of breadcrumbs for exceptions happens inside the .NET SDK + return; + } + + // Capture so the next event includes this as breadcrumb + if (_options.AddBreadcrumbsForLogType.TryGetValue(logType, out var value) && value) + { + _hub?.AddBreadcrumb(message: message, category: "unity.logger", level: ToBreadcrumbLevel(logType)); + } } + private void OnQuitting() => _application.LogMessageReceived -= OnLogMessageReceived; + private static BreadcrumbLevel ToBreadcrumbLevel(LogType logType) => logType switch { diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 90e5808fb..7b0567856 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -58,6 +58,15 @@ public static string GetConfigPath(string? notDefaultConfigName = null) [field: SerializeField] public int MaxViewHierarchyObjectChildCount { get; set; } = 20; [field: SerializeField] public int MaxViewHierarchyDepth { get; set; } = 10; + [field: SerializeField] public bool EnableStructuredLogging { get; set; } = false; + [field: SerializeField] public bool OnDebugLog { get; set; } = false; + [field: SerializeField] public bool OnDebugLogWarning { get; set; } = true; + [field: SerializeField] public bool OnDebugLogAssertion { get; set; } = true; + [field: SerializeField] public bool OnDebugLogError { get; set; } = true; + [field: SerializeField] public bool OnDebugLogException { get; set; } = true; + + [field: SerializeField] public bool AttachBreadcrumbsToEvents { get; set; } = false; + [field: SerializeField] public bool BreadcrumbsForLogs { get; set; } = true; [field: SerializeField] public bool BreadcrumbsForWarnings { get; set; } = true; [field: SerializeField] public bool BreadcrumbsForAsserts { get; set; } = true; @@ -184,6 +193,16 @@ internal SentryUnityOptions ToSentryUnityOptions( XboxNativeSupportEnabled = XboxNativeSupportEnabled, Il2CppLineNumberSupportEnabled = Il2CppLineNumberSupportEnabled, PerformanceAutoInstrumentationEnabled = AutoAwakeTraces, + Experimental = new SentryUnityExperimentalOptions + { + EnableLogs = EnableStructuredLogging, + OnDebugLog = OnDebugLog, + OnDebugLogWarning = OnDebugLogWarning, + OnDebugLogAssertion = OnDebugLogAssertion, + OnDebugLogError = OnDebugLogError, + OnDebugLogException = OnDebugLogException, + AttachBreadcrumbsToEvents = AttachBreadcrumbsToEvents + } }; // By default, the cacheDirectoryPath gets set on known platforms. We're overwriting this behaviour here. diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index e0008f79c..12426d7d5 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -305,6 +305,9 @@ internal string? DefaultUserId internal ISentryUnityInfo UnityInfo { get; private set; } internal Action? PlatformConfiguration { get; private set; } + // Hiding the .NET one + public new SentryUnityExperimentalOptions Experimental { get; set; } + public SentryUnityOptions() : this(isBuilding: false) { } // For testing @@ -313,6 +316,8 @@ internal SentryUnityOptions(IApplication? application = null, ISentryUnityInfo? unityInfo = null, bool isBuilding = false) { + // Initialize with Unity-specific experimental options + Experimental = new SentryUnityExperimentalOptions(); // NOTE: 'SentryPlatformServices.UnityInfo' throws when the UnityInfo has not been set. This should not happen. // The PlatformServices are set through the RuntimeLoad attribute in 'SentryInitialization.cs' and are required // to be present. @@ -490,3 +495,27 @@ public enum NativeInitializationType /// BuildTime, } + +/// +/// Unity-specific experimental options. +/// +/// +/// This extends the base with Unity-specific experimental features. +/// These options are subject to change in future versions. +/// +public sealed class SentryUnityExperimentalOptions : SentryOptions.SentryExperimentalOptions +{ + internal SentryUnityExperimentalOptions() { } + + public bool OnDebugLog { get; set; } = false; + public bool OnDebugLogWarning { get; set; } = true; + public bool OnDebugLogAssertion { get; set; } = true; + public bool OnDebugLogError { get; set; } = true; + public bool OnDebugLogException { get; set; } = true; + + /// + /// When set to true, breadcrumbs will be added to the top of the breadcrumb list instead of the bottom. + /// Defaults to false. + /// + public bool AttachBreadcrumbsToEvents { get; set; } = false; +} From e2ae0959a5508931dbbc06758f7dd23e7399b316 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 22 Oct 2025 16:53:07 +0200 Subject: [PATCH 03/18] Updated sample LogError message --- samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs | 2 +- samples/unity-of-bugs/Assets/Scripts/ThreadingSamples.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs b/samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs index 9cc7a814b..e0a3d072b 100644 --- a/samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs +++ b/samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs @@ -71,7 +71,7 @@ public void LogError() // Error logs get captured as messages and do not have a stacktrace attached by default. This is an opt-in feature. // Note: That stack traces generated for message events are provided without line numbers. See known limitations // https://docs.sentry.io/platforms/unity/troubleshooting/known-limitations/#line-numbers-missing-in-events-captured-through-debuglogerror-or-sentrysdkcapturemessage - Debug.LogError("Debug.LogError() called"); + Debug.LogError("This is a 'Debug.LogError()' message."); } } diff --git a/samples/unity-of-bugs/Assets/Scripts/ThreadingSamples.cs b/samples/unity-of-bugs/Assets/Scripts/ThreadingSamples.cs index 098446ffa..54575a7ea 100644 --- a/samples/unity-of-bugs/Assets/Scripts/ThreadingSamples.cs +++ b/samples/unity-of-bugs/Assets/Scripts/ThreadingSamples.cs @@ -81,7 +81,7 @@ public void LogError() // Error logs get captured as messages and do not have a stacktrace attached by default. This is an opt-in feature. // Note: That stack traces generated for message events are provided without line numbers. See known limitations // https://docs.sentry.io/platforms/unity/troubleshooting/known-limitations/#line-numbers-missing-in-events-captured-through-debuglogerror-or-sentrysdkcapturemessage - Debug.LogError("Debug.LogError() called"); + Debug.LogError("This is a 'Debug.LogError()' message."); } }); } From 3da5fc9a38f45df3cab6583bb129191eeb8db95b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 22 Oct 2025 16:57:07 +0200 Subject: [PATCH 04/18] Opt-in check for breadcrumb collection --- .../Integrations/UnityApplicationLoggingIntegration.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index 479f1595f..dfeec8cf8 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -155,6 +155,12 @@ private void ProcessBreadcrumbs(string message, LogType logType) return; } + // Breadcrumb collection on top of structure log capture must be opted in + if (_options.Experimental is { EnableLogs: true, AttachBreadcrumbsToEvents: false }) + { + return; + } + // Capture so the next event includes this as breadcrumb if (_options.AddBreadcrumbsForLogType.TryGetValue(logType, out var value) && value) { From 7834bc03f6d5012222fab1e621e396a31d3b6f29 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 23 Oct 2025 10:29:43 +0200 Subject: [PATCH 05/18] Added logging --- .../UnityApplicationLoggingIntegration.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index dfeec8cf8..1a73c71e7 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -1,4 +1,5 @@ using System; +using Sentry.Extensibility; using Sentry.Integrations; using UnityEngine; @@ -49,11 +50,17 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo } // We're not capturing the SDKs own logs - if (message.StartsWith(UnityLogger.LogTag) || IsGettingDebounced(logType)) + if (message.StartsWith(UnityLogger.LogTag)) { return; } + if (IsGettingDebounced(logType)) + { + _options.LogDebug("Log message of type '{0}' is getting debounced.", logType); + return; + } + ProcessStructuredLog(message, logType); ProcessException(message, stacktrace, logType); ProcessError(message, stacktrace, logType); @@ -62,12 +69,12 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo private bool IsGettingDebounced(LogType logType) { - if (_options?.EnableLogDebouncing is not true) + if (_options.EnableLogDebouncing is false) { return false; } - var debounced = logType switch + return logType switch { LogType.Exception => _errorTimeDebounce.Debounced(), LogType.Error or LogType.Assert => _errorTimeDebounce.Debounced(), @@ -75,8 +82,6 @@ private bool IsGettingDebounced(LogType logType) LogType.Warning => _warningTimeDebounce.Debounced(), _ => true }; - - return debounced; } private void ProcessStructuredLog(string message, LogType logType) @@ -86,30 +91,35 @@ private void ProcessStructuredLog(string message, LogType logType) case LogType.Log: if (_options.Experimental.OnDebugLog) { + _options.LogDebug("Capturing structured log message of type '{0}'", logType); Sentry.SentrySdk.Logger.LogInfo(message); } break; case LogType.Warning: if (_options.Experimental.OnDebugLogWarning) { + _options.LogDebug("Capturing structured log message of type '{0}'", logType); Sentry.SentrySdk.Logger.LogWarning(message); } break; case LogType.Assert: if (_options.Experimental.OnDebugLogAssertion) { + _options.LogDebug("Capturing structured log message of type '{0}'", logType); Sentry.SentrySdk.Logger.LogError(message); } break; case LogType.Error: if (_options.Experimental.OnDebugLogError) { + _options.LogDebug("Capturing structured log message of type '{0}'", logType); Sentry.SentrySdk.Logger.LogError(message); } break; case LogType.Exception: if (_options.Experimental.OnDebugLogException) { + _options.LogDebug("Capturing structured log message of type '{0}'", logType); Sentry.SentrySdk.Logger.LogError(message); } break; @@ -122,6 +132,8 @@ private void ProcessException(string message, string stacktrace, LogType logType // UNLESS we're configured to handle them - i.e. WebGL if (logType is LogType.Exception && _captureExceptions) { + _options.LogDebug("Exception capture has been enabled. Capturing exception through '{0}'.", nameof(UnityApplicationLoggingIntegration)); + var ule = new UnityErrorLogException(message, stacktrace, _options); _hub?.CaptureException(ule); } @@ -134,8 +146,12 @@ private void ProcessError(string message, string stacktrace, LogType logType) return; } + _options.LogDebug("Error capture for 'Debug.LogError' has been enabled. Capturing message."); + if (_options.AttachStacktrace && !string.IsNullOrEmpty(stacktrace)) { + _options.LogDebug("Attaching stacktrace to event."); + var ule = new UnityErrorLogException(message, stacktrace, _options); var sentryEvent = new SentryEvent(ule) { Level = SentryLevel.Error }; @@ -164,6 +180,7 @@ private void ProcessBreadcrumbs(string message, LogType logType) // Capture so the next event includes this as breadcrumb if (_options.AddBreadcrumbsForLogType.TryGetValue(logType, out var value) && value) { + _options.LogDebug("Adding breadcrumb for log message of type: {0}", logType); _hub?.AddBreadcrumb(message: message, category: "unity.logger", level: ToBreadcrumbLevel(logType)); } } From 34823dc3ab761c264394b0989ae53f273bdc77a9 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 23 Oct 2025 10:30:05 +0200 Subject: [PATCH 06/18] Options --- .../unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset index e04678b85..17acd40a1 100644 --- a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset +++ b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset @@ -27,7 +27,7 @@ MonoBehaviour: k__BackingField: 30000 k__BackingField: k__BackingField: - k__BackingField: 0 + k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 k__BackingField: 75 @@ -41,7 +41,7 @@ MonoBehaviour: k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 - k__BackingField: 1 + k__BackingField: 0 k__BackingField: 1 k__BackingField: 1 k__BackingField: 1 From 73eb7ed0c36a8555a38d5ffb211827eccf0f340e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 12:56:06 +0200 Subject: [PATCH 07/18] Wrapup for Logging --- .../UnityApplicationLoggingIntegration.cs | 48 +-------- .../Integrations/UnityErrorLogException.cs | 1 + .../UnityLogHandlerIntegration.cs | 97 +++++++++++++++---- src/Sentry.Unity/SentryUnityOptions.cs | 15 ++- .../UnityLogHandlerIntegrationTests.cs | 4 +- 5 files changed, 93 insertions(+), 72 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index 1a73c71e7..d55aec44a 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -61,7 +61,6 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo return; } - ProcessStructuredLog(message, logType); ProcessException(message, stacktrace, logType); ProcessError(message, stacktrace, logType); ProcessBreadcrumbs(message, logType); @@ -84,52 +83,10 @@ private bool IsGettingDebounced(LogType logType) }; } - private void ProcessStructuredLog(string message, LogType logType) - { - switch (logType) - { - case LogType.Log: - if (_options.Experimental.OnDebugLog) - { - _options.LogDebug("Capturing structured log message of type '{0}'", logType); - Sentry.SentrySdk.Logger.LogInfo(message); - } - break; - case LogType.Warning: - if (_options.Experimental.OnDebugLogWarning) - { - _options.LogDebug("Capturing structured log message of type '{0}'", logType); - Sentry.SentrySdk.Logger.LogWarning(message); - } - break; - case LogType.Assert: - if (_options.Experimental.OnDebugLogAssertion) - { - _options.LogDebug("Capturing structured log message of type '{0}'", logType); - Sentry.SentrySdk.Logger.LogError(message); - } - break; - case LogType.Error: - if (_options.Experimental.OnDebugLogError) - { - _options.LogDebug("Capturing structured log message of type '{0}'", logType); - Sentry.SentrySdk.Logger.LogError(message); - } - break; - case LogType.Exception: - if (_options.Experimental.OnDebugLogException) - { - _options.LogDebug("Capturing structured log message of type '{0}'", logType); - Sentry.SentrySdk.Logger.LogError(message); - } - break; - } - } - private void ProcessException(string message, string stacktrace, LogType logType) { // LogType.Exception is getting handled by the `UnityLogHandlerIntegration` - // UNLESS we're configured to handle them - i.e. WebGL + // UNLESS we're configured to handle them - i.e. on WebGL if (logType is LogType.Exception && _captureExceptions) { _options.LogDebug("Exception capture has been enabled. Capturing exception through '{0}'.", nameof(UnityApplicationLoggingIntegration)); @@ -146,7 +103,7 @@ private void ProcessError(string message, string stacktrace, LogType logType) return; } - _options.LogDebug("Error capture for 'Debug.LogError' has been enabled. Capturing message."); + _options.LogDebug("Error capture for 'Debug.LogError' is enabled. Capturing message."); if (_options.AttachStacktrace && !string.IsNullOrEmpty(stacktrace)) { @@ -177,7 +134,6 @@ private void ProcessBreadcrumbs(string message, LogType logType) return; } - // Capture so the next event includes this as breadcrumb if (_options.AddBreadcrumbsForLogType.TryGetValue(logType, out var value) && value) { _options.LogDebug("Adding breadcrumb for log message of type: {0}", logType); diff --git a/src/Sentry.Unity/Integrations/UnityErrorLogException.cs b/src/Sentry.Unity/Integrations/UnityErrorLogException.cs index 8d7a6daeb..c1dcf6237 100644 --- a/src/Sentry.Unity/Integrations/UnityErrorLogException.cs +++ b/src/Sentry.Unity/Integrations/UnityErrorLogException.cs @@ -24,6 +24,7 @@ internal class UnityErrorLogException : Exception private readonly IDiagnosticLogger? _logger; public UnityErrorLogException(string logString, string logStackTrace, SentryOptions? options) + : base(logString) { _logString = logString; _logStackTrace = logStackTrace; diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index b93b15a1a..68d0e56fc 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -13,13 +13,11 @@ namespace Sentry.Unity.Integrations; internal sealed class UnityLogHandlerIntegration : ISdkIntegration, ILogHandler { private readonly IApplication _application; - private IHub? _hub; - private SentryUnityOptions? _sentryOptions; - + private SentryUnityOptions _options = null!; // Set during register private ILogHandler _unityLogHandler = null!; // Set during register - public UnityLogHandlerIntegration(SentryUnityOptions options, IApplication? application = null) + public UnityLogHandlerIntegration(IApplication? application = null) { _application = application ?? ApplicationAdapter.Instance; } @@ -27,17 +25,14 @@ public UnityLogHandlerIntegration(SentryUnityOptions options, IApplication? appl public void Register(IHub hub, SentryOptions sentryOptions) { _hub = hub; - _sentryOptions = sentryOptions as SentryUnityOptions; - if (_sentryOptions is null) - { - return; - } + // This should never happen, but if it does... + _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options is not of type 'SentryUnityOptions'."); // If called twice (i.e. init with the same options object) the integration will reference itself as the // original handler loghandler and endlessly forward to itself if (Debug.unityLogger.logHandler == this) { - _sentryOptions.DiagnosticLogger?.LogWarning("UnityLogHandlerIntegration has already been registered."); + _options.DiagnosticLogger?.LogWarning("UnityLogHandlerIntegration has already been registered."); return; } @@ -51,7 +46,7 @@ public void LogException(Exception exception, UnityEngine.Object context) { try { - CaptureException(exception, context); + ProcessException(exception, context); } finally { @@ -60,7 +55,7 @@ public void LogException(Exception exception, UnityEngine.Object context) } } - internal void CaptureException(Exception exception, UnityEngine.Object? context) + internal void ProcessException(Exception exception, UnityEngine.Object? context) { if (_hub?.IsEnabled is not true) { @@ -75,19 +70,83 @@ internal void CaptureException(Exception exception, UnityEngine.Object? context) exception.Data[Mechanism.HandledKey] = false; exception.Data[Mechanism.MechanismKey] = "Unity.LogException"; _ = _hub.CaptureException(exception); + + if (_options.Experimental.OnDebugLogException) + { + _options.LogDebug("Capturing structured log message of type '{0}'.", LogType.Exception); + Sentry.SentrySdk.Logger.LogError(exception.Message); + } } public void LogFormat(LogType logType, UnityEngine.Object? context, string format, params object[] args) { - // Always pass the log back to Unity - // Capturing of `Debug`, `Warning`, and `Error` happens in the Application Logging Integration. - // The LogHandler does not have access to the stacktrace information required - _unityLogHandler.LogFormat(logType, context, format, args); + try + { + ProcessLog(logType, context, format, args); + var message = string.Format(format, args); + } + finally + { + // Always pass the log back to Unity + // Capturing of `Debug`, `Warning`, and `Error` happens in the Application Logging Integration. + // The LogHandler does not have access to the stacktrace information required + _unityLogHandler.LogFormat(logType, context, format, args); + } + } + + private void ProcessLog(LogType logType, UnityEngine.Object? context, string format, params object[] args) + { + if (_hub?.IsEnabled is not true) + { + return; + } + + if (args.Length > 1 && args[0] is UnityLogger.LogTag) + { + return; + } + + ProcessStructuredLog(logType, format, args); + } + + private void ProcessStructuredLog(LogType logType, string format, params object[] args) + { + switch (logType) + { + case LogType.Log: + if (_options.Experimental.OnDebugLog) + { + _options.LogDebug("Capturing structured log message of type '{0}'.", logType); + Sentry.SentrySdk.Logger.LogInfo(format, args); + } + break; + case LogType.Warning: + if (_options.Experimental.OnDebugLogWarning) + { + _options.LogDebug("Capturing structured log message of type '{0}'.", logType); + Sentry.SentrySdk.Logger.LogWarning(format, args); + } + break; + case LogType.Assert: + if (_options.Experimental.OnDebugLogAssertion) + { + _options.LogDebug("Capturing structured log message of type '{0}'.", logType); + Sentry.SentrySdk.Logger.LogError(format, args); + } + break; + case LogType.Error: + if (_options.Experimental.OnDebugLogError) + { + _options.LogDebug("Capturing structured log message of type '{0}'.", logType); + Sentry.SentrySdk.Logger.LogError(format, args); + } + break; + } } private void OnQuitting() { - _sentryOptions?.DiagnosticLogger?.LogInfo("OnQuitting was invoked. Unhooking log callback and pausing session."); + _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. @@ -97,10 +156,10 @@ private void OnQuitting() // '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 (_sentryOptions?.AutoSessionTracking is true) + if (_options.AutoSessionTracking) { _hub?.PauseSession(); } - _hub?.FlushAsync(_sentryOptions?.ShutdownTimeout ?? TimeSpan.FromSeconds(1)).GetAwaiter().GetResult(); + _hub?.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); } } diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 12426d7d5..862d8725b 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -305,8 +305,12 @@ internal string? DefaultUserId internal ISentryUnityInfo UnityInfo { get; private set; } internal Action? PlatformConfiguration { get; private set; } - // Hiding the .NET one - public new SentryUnityExperimentalOptions Experimental { get; set; } + // Delegate to base property to ensure both Unity and base SDK reference the same instance + public new SentryUnityExperimentalOptions Experimental + { + get => (SentryUnityExperimentalOptions)base.Experimental; + set => base.Experimental = value; + } public SentryUnityOptions() : this(isBuilding: false) { } @@ -316,14 +320,15 @@ internal SentryUnityOptions(IApplication? application = null, ISentryUnityInfo? unityInfo = null, bool isBuilding = false) { - // Initialize with Unity-specific experimental options - Experimental = new SentryUnityExperimentalOptions(); // NOTE: 'SentryPlatformServices.UnityInfo' throws when the UnityInfo has not been set. This should not happen. // The PlatformServices are set through the RuntimeLoad attribute in 'SentryInitialization.cs' and are required // to be present. UnityInfo = unityInfo ?? SentryPlatformServices.UnityInfo; PlatformConfiguration = SentryPlatformServices.PlatformConfiguration; + // Initialize base.Experimental with Unity-specific experimental options + base.Experimental = new SentryUnityExperimentalOptions(); + application ??= ApplicationAdapter.Instance; behaviour ??= SentryMonoBehaviour.Instance; @@ -343,7 +348,7 @@ internal SentryUnityOptions(IApplication? application = null, // UnityLogHandlerIntegration is not compatible with WebGL, so it's added conditionally if (application.Platform != RuntimePlatform.WebGLPlayer) { - AddIntegration(new UnityLogHandlerIntegration(this)); + AddIntegration(new UnityLogHandlerIntegration()); AddIntegration(new UnityApplicationLoggingIntegration()); } diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index 9070daa95..a4f744c50 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -19,7 +19,7 @@ private class Fixture public UnityLogHandlerIntegration GetSut() { var application = new TestApplication(); - var integration = new UnityLogHandlerIntegration(SentryOptions, application); + var integration = new UnityLogHandlerIntegration(application); integration.Register(Hub, SentryOptions); return integration; } @@ -43,7 +43,7 @@ public void CaptureException_ExceptionCapturedAndMechanismSet() var sut = _fixture.GetSut(); var message = NUnit.Framework.TestContext.CurrentContext.Test.Name; - sut.CaptureException(new Exception(message), null); + sut.ProcessException(new Exception(message), null); Assert.AreEqual(1, _fixture.Hub.CapturedEvents.Count); From ee9dfc0079bad2c03ee0d09c5d77136b6182682f Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 13:00:02 +0200 Subject: [PATCH 08/18] Fixed debounce logic --- .../Integrations/UnityApplicationLoggingIntegration.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index d55aec44a..5d82dd58f 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -75,10 +75,10 @@ private bool IsGettingDebounced(LogType logType) return logType switch { - LogType.Exception => _errorTimeDebounce.Debounced(), - LogType.Error or LogType.Assert => _errorTimeDebounce.Debounced(), - LogType.Log => _logTimeDebounce.Debounced(), - LogType.Warning => _warningTimeDebounce.Debounced(), + LogType.Exception => !_errorTimeDebounce.Debounced(), + LogType.Error or LogType.Assert => !_errorTimeDebounce.Debounced(), + LogType.Log => !_logTimeDebounce.Debounced(), + LogType.Warning => !_warningTimeDebounce.Debounced(), _ => true }; } From dc8a638ec34442433d839830701624a5df17e3b8 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 13:46:19 +0200 Subject: [PATCH 09/18] Interation on working base --- .../UnityLogHandlerIntegration.cs | 38 ++++----- .../ScriptableSentryUnityOptions.cs | 13 +-- src/Sentry.Unity/SentryUnityOptions.cs | 25 ++++-- .../Stubs/TestStructuredLogger.cs | 24 ++++++ ...UnityApplicationLoggingIntegrationTests.cs | 73 ++++++++++++++++ .../UnityLogHandlerIntegrationTests.cs | 83 +++++++++++++++++++ 6 files changed, 219 insertions(+), 37 deletions(-) create mode 100644 test/Sentry.Unity.Tests/Stubs/TestStructuredLogger.cs diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index 68d0e56fc..4c4f59068 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -16,6 +16,7 @@ internal sealed class UnityLogHandlerIntegration : ISdkIntegration, ILogHandler private IHub? _hub; private SentryUnityOptions _options = null!; // Set during register private ILogHandler _unityLogHandler = null!; // Set during register + internal SentryStructuredLogger _structuredLogger = null!; // Set during register public UnityLogHandlerIntegration(IApplication? application = null) { @@ -27,6 +28,7 @@ public void Register(IHub hub, SentryOptions sentryOptions) _hub = hub; // This should never happen, but if it does... _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options is not of type 'SentryUnityOptions'."); + _structuredLogger = Sentry.SentrySdk.Logger; // If called twice (i.e. init with the same options object) the integration will reference itself as the // original handler loghandler and endlessly forward to itself @@ -71,10 +73,10 @@ internal void ProcessException(Exception exception, UnityEngine.Object? context) exception.Data[Mechanism.MechanismKey] = "Unity.LogException"; _ = _hub.CaptureException(exception); - if (_options.Experimental.OnDebugLogException) + if (_options.Experimental.CaptureStructuredLogsForLogType.TryGetValue(LogType.Exception, out var captureException) && captureException) { _options.LogDebug("Capturing structured log message of type '{0}'.", LogType.Exception); - Sentry.SentrySdk.Logger.LogError(exception.Message); + _structuredLogger.LogError(exception.Message); } } @@ -83,7 +85,6 @@ public void LogFormat(LogType logType, UnityEngine.Object? context, string forma try { ProcessLog(logType, context, format, args); - var message = string.Format(format, args); } finally { @@ -111,35 +112,24 @@ private void ProcessLog(LogType logType, UnityEngine.Object? context, string for private void ProcessStructuredLog(LogType logType, string format, params object[] args) { + if (!_options.Experimental.CaptureStructuredLogsForLogType.TryGetValue(logType, out var captureLog) || !captureLog) + { + return; + } + + _options.LogDebug("Capturing structured log message of type '{0}'.", logType); + switch (logType) { case LogType.Log: - if (_options.Experimental.OnDebugLog) - { - _options.LogDebug("Capturing structured log message of type '{0}'.", logType); - Sentry.SentrySdk.Logger.LogInfo(format, args); - } + _structuredLogger.LogInfo(format, args); break; case LogType.Warning: - if (_options.Experimental.OnDebugLogWarning) - { - _options.LogDebug("Capturing structured log message of type '{0}'.", logType); - Sentry.SentrySdk.Logger.LogWarning(format, args); - } + _structuredLogger.LogWarning(format, args); break; case LogType.Assert: - if (_options.Experimental.OnDebugLogAssertion) - { - _options.LogDebug("Capturing structured log message of type '{0}'.", logType); - Sentry.SentrySdk.Logger.LogError(format, args); - } - break; case LogType.Error: - if (_options.Experimental.OnDebugLogError) - { - _options.LogDebug("Capturing structured log message of type '{0}'.", logType); - Sentry.SentrySdk.Logger.LogError(format, args); - } + _structuredLogger.LogError(format, args); break; } } diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 7b0567856..3d6b0366b 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -196,11 +196,14 @@ internal SentryUnityOptions ToSentryUnityOptions( Experimental = new SentryUnityExperimentalOptions { EnableLogs = EnableStructuredLogging, - OnDebugLog = OnDebugLog, - OnDebugLogWarning = OnDebugLogWarning, - OnDebugLogAssertion = OnDebugLogAssertion, - OnDebugLogError = OnDebugLogError, - OnDebugLogException = OnDebugLogException, + CaptureStructuredLogsForLogType = + { + [LogType.Log] = OnDebugLog, + [LogType.Warning] = OnDebugLogWarning, + [LogType.Assert] = OnDebugLogAssertion, + [LogType.Error] = OnDebugLogError, + [LogType.Exception] = OnDebugLogException + }, AttachBreadcrumbsToEvents = AttachBreadcrumbsToEvents } }; diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 862d8725b..02e6ba26a 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -510,17 +510,26 @@ public enum NativeInitializationType /// public sealed class SentryUnityExperimentalOptions : SentryOptions.SentryExperimentalOptions { - internal SentryUnityExperimentalOptions() { } - - public bool OnDebugLog { get; set; } = false; - public bool OnDebugLogWarning { get; set; } = true; - public bool OnDebugLogAssertion { get; set; } = true; - public bool OnDebugLogError { get; set; } = true; - public bool OnDebugLogException { get; set; } = true; + /// + /// Controls whether structured logs should be captured for each Unity log type. + /// + public Dictionary CaptureStructuredLogsForLogType { get; set; } /// - /// When set to true, breadcrumbs will be added to the top of the breadcrumb list instead of the bottom. + /// When set to true, breadcrumbs will be added on top of structured logging. /// Defaults to false. /// public bool AttachBreadcrumbsToEvents { get; set; } = false; + + internal SentryUnityExperimentalOptions() + { + CaptureStructuredLogsForLogType = new Dictionary + { + { LogType.Log, false }, + { LogType.Warning, true }, + { LogType.Assert, true }, + { LogType.Error, true }, + { LogType.Exception, true } + }; + } } diff --git a/test/Sentry.Unity.Tests/Stubs/TestStructuredLogger.cs b/test/Sentry.Unity.Tests/Stubs/TestStructuredLogger.cs new file mode 100644 index 000000000..52b5c10ba --- /dev/null +++ b/test/Sentry.Unity.Tests/Stubs/TestStructuredLogger.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Sentry.Unity.Tests.Stubs; + +internal sealed class TestStructuredLogger : SentryStructuredLogger +{ + public List<(string level, string message, object[] args)> LogCalls { get; } = new(); + + private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action? configureLog) + => LogCalls.Add((level.ToString(), template, parameters ?? [])); + + protected internal override void CaptureLog(SentryLog log) + { + // Not needed for our tests + } + + protected internal override void Flush() + { + // Not needed for our tests + } + + public void Clear() => LogCalls.Clear(); +} diff --git a/test/Sentry.Unity.Tests/UnityApplicationLoggingIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityApplicationLoggingIntegrationTests.cs index 03f948a24..7654d1644 100644 --- a/test/Sentry.Unity.Tests/UnityApplicationLoggingIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityApplicationLoggingIntegrationTests.cs @@ -175,5 +175,78 @@ public void OnLogMessageReceived_LogTypeException_CaptureExceptionsEnabled_Event Assert.AreEqual(1, _fixture.Hub.CapturedEvents.Count); } + + [Test] + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Error)] + public void OnLogMessageReceived_ExperimentalLogsEnabledWithAttachBreadcrumbsFalse_BreadcrumbsNotAdded(LogType unityLogType) + { + _fixture.SentryOptions.Experimental.EnableLogs = true; + _fixture.SentryOptions.Experimental.AttachBreadcrumbsToEvents = false; + _fixture.SentryOptions.AddBreadcrumbsForLogType[unityLogType] = true; + var sut = _fixture.GetSut(); + var message = TestContext.CurrentContext.Test.Name; + + sut.OnLogMessageReceived(message, string.Empty, unityLogType); + + Assert.AreEqual(0, _fixture.Hub.ConfigureScopeCalls.Count); + } + + [Test] + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Error)] + public void OnLogMessageReceived_ExperimentalLogsEnabledWithAttachBreadcrumbsTrue_BreadcrumbsAdded(LogType unityLogType) + { + _fixture.SentryOptions.Experimental.EnableLogs = true; + _fixture.SentryOptions.Experimental.AttachBreadcrumbsToEvents = true; + _fixture.SentryOptions.AddBreadcrumbsForLogType[unityLogType] = true; + var sut = _fixture.GetSut(); + var message = TestContext.CurrentContext.Test.Name; + + sut.OnLogMessageReceived(message, string.Empty, unityLogType); + + var scope = new Scope(_fixture.SentryOptions); + _fixture.Hub.ConfigureScopeCalls.Single().Invoke(scope); + var breadcrumb = scope.Breadcrumbs.Single(); + + Assert.AreEqual(message, breadcrumb.Message); + Assert.AreEqual("unity.logger", breadcrumb.Category); + } + + [Test] + [TestCase(LogType.Log)] + [TestCase(LogType.Warning)] + [TestCase(LogType.Error)] + public void OnLogMessageReceived_ExperimentalLogsDisabled_BreadcrumbsAddedAsNormal(LogType unityLogType) + { + _fixture.SentryOptions.Experimental.EnableLogs = false; + _fixture.SentryOptions.AddBreadcrumbsForLogType[unityLogType] = true; + var sut = _fixture.GetSut(); + var message = TestContext.CurrentContext.Test.Name; + + sut.OnLogMessageReceived(message, string.Empty, unityLogType); + + var scope = new Scope(_fixture.SentryOptions); + _fixture.Hub.ConfigureScopeCalls.Single().Invoke(scope); + var breadcrumb = scope.Breadcrumbs.Single(); + + Assert.AreEqual(message, breadcrumb.Message); + Assert.AreEqual("unity.logger", breadcrumb.Category); + } + + [Test] + public void OnLogMessageReceived_ExceptionType_NoBreadcrumbAdded() + { + _fixture.SentryOptions.AddBreadcrumbsForLogType[LogType.Exception] = true; + var sut = _fixture.GetSut(); + var message = TestContext.CurrentContext.Test.Name; + + sut.OnLogMessageReceived(message, "stacktrace", LogType.Exception); + + // Exception breadcrumbs are handled by the .NET SDK, not by this integration + Assert.AreEqual(0, _fixture.Hub.ConfigureScopeCalls.Count); + } } } diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index a4f744c50..4a5653069 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -6,6 +6,7 @@ using Sentry.Unity.Tests.SharedClasses; using Sentry.Unity.Tests.Stubs; using UnityEngine; +using UnityEngine.TestTools; namespace Sentry.Unity.Tests; @@ -75,4 +76,86 @@ public void Register_RegisteredASecondTime_LogsWarningAndReturns() log.logLevel == SentryLevel.Warning && log.message.Contains("UnityLogHandlerIntegration has already been registered."))); } + + [Test] + public void ProcessException_ExperimentalCaptureEnabled_CapturesStructuredLog() + { + _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Exception] = true; + var sut = _fixture.GetSut(); + var testStructuredLogger = new TestStructuredLogger(); + sut._structuredLogger = testStructuredLogger; + var message = TestContext.CurrentContext.Test.Name; + + sut.ProcessException(new Exception(message), null); + + Assert.AreEqual(1, testStructuredLogger.LogCalls.Count); + var logCall = testStructuredLogger.LogCalls.Single(); + Assert.AreEqual("Error", logCall.level); + Assert.AreEqual(message, logCall.message); + } + + [Test] + public void ProcessException_ExperimentalCaptureDisabled_DoesNotCaptureStructuredLog() + { + _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Exception] = false; + var sut = _fixture.GetSut(); + var testStructuredLogger = new TestStructuredLogger(); + sut._structuredLogger = testStructuredLogger; + var message = TestContext.CurrentContext.Test.Name; + + sut.ProcessException(new Exception(message), null); + + Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + } + + [Test] + public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() + { + _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Error] = true; + var sut = _fixture.GetSut(); + var testStructuredLogger = new TestStructuredLogger(); + sut._structuredLogger = testStructuredLogger; + + const string? format = "{0}: {1}"; + const string? message = "Test message"; + LogAssert.Expect(LogType.Error, string.Format(format, UnityLogger.LogTag, message)); + + sut.LogFormat(LogType.Error, null, format, UnityLogger.LogTag, message); + + Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + } + + [Test] + [TestCase(LogType.Log, "Info", true)] + [TestCase(LogType.Log, "Info", false)] + [TestCase(LogType.Warning, "Warning", true)] + [TestCase(LogType.Warning, "Warning", false)] + [TestCase(LogType.Error, "Error", true)] + [TestCase(LogType.Error, "Error", false)] + [TestCase(LogType.Assert, "Error", true)] + [TestCase(LogType.Assert, "Error", false)] + public void LogFormat_WithExperimentalFlag_CapturesStructuredLogWhenEnabled(LogType logType, string expectedLevel, bool captureEnabled) + { + _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[logType] = captureEnabled; + var sut = _fixture.GetSut(); + var testStructuredLogger = new TestStructuredLogger(); + sut._structuredLogger = testStructuredLogger; + var message = TestContext.CurrentContext.Test.Name; + + LogAssert.Expect(logType, message); + + sut.LogFormat(logType, null, message); + + if (captureEnabled) + { + Assert.AreEqual(1, testStructuredLogger.LogCalls.Count); + var logCall = testStructuredLogger.LogCalls.Single(); + Assert.AreEqual(expectedLevel, logCall.level); + Assert.AreEqual(message, logCall.message); + } + else + { + Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + } + } } From 90b12a8ef968687a934e4d04d8483d18c97a10cc Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 13:48:53 +0200 Subject: [PATCH 10/18] Added beforesend --- src/Sentry.Unity/SentryUnityOptions.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 02e6ba26a..2555296a4 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -521,6 +521,20 @@ public sealed class SentryUnityExperimentalOptions : SentryOptions.SentryExperim /// public bool AttachBreadcrumbsToEvents { get; set; } = false; + /// + /// Sets a callback function to be invoked before sending the log to Sentry. + /// When the delegate throws an during invocation, the log will not be captured. + /// + /// + /// It can be used to modify the log object before being sent to Sentry. + /// To prevent the log from being sent to Sentry, return . + /// + /// + public new void SetBeforeSendLog(Func beforeSendLog) + { + base.SetBeforeSendLog(beforeSendLog); + } + internal SentryUnityExperimentalOptions() { CaptureStructuredLogsForLogType = new Dictionary From ddc9605336891b0241ed93c2fa3a473684bab5f2 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 13:51:26 +0200 Subject: [PATCH 11/18] Added logger to Unity SDK static API --- src/Sentry.Unity/SentrySdk.Dotnet.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Sentry.Unity/SentrySdk.Dotnet.cs b/src/Sentry.Unity/SentrySdk.Dotnet.cs index b4ea8fb07..1248f3a11 100644 --- a/src/Sentry.Unity/SentrySdk.Dotnet.cs +++ b/src/Sentry.Unity/SentrySdk.Dotnet.cs @@ -51,6 +51,17 @@ public static partial class SentrySdk /// public static bool IsEnabled { [DebuggerStepThrough] get => Sentry.SentrySdk.IsEnabled; } + /// + /// Gets the structured logger instance for creating and sending logs to Sentry. + /// + /// + /// Use this property to access structured logging functionality. Logs are only sent when + /// 's + /// is set to true. + /// + /// + public static SentryStructuredLogger Logger { [DebuggerStepThrough] get => Sentry.SentrySdk.Logger; } + /// /// Creates a new scope that will terminate when disposed. /// From e62733c7de96ad742f27ee3b1eb1caa4d78eff6d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 13:54:15 +0200 Subject: [PATCH 12/18] Updated CHANGELOG.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047904e22..a89e9fdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ # Changelog -## Unreleased +## Unreleased ### Breaking Changes - `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)) +### 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)) + ### Dependencies - Bump CLI from v2.56.0 to v2.56.1 ([#2356](https://github.com/getsentry/sentry-unity/pull/2356)) From 7f9950dbb9b88c2c4e73b910a80ebb3700b525f3 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 14:07:03 +0200 Subject: [PATCH 13/18] Don't access internal structured logger --- .../UnityApplicationLoggingIntegration.cs | 2 +- .../UnityLogHandlerIntegration.cs | 15 +++++++-- .../UnityLogHandlerIntegrationTests.cs | 31 +++++++++---------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index 5d82dd58f..8b4460e97 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -49,7 +49,7 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo return; } - // We're not capturing the SDKs own logs + // We're not capturing the SDK's own logs if (message.StartsWith(UnityLogger.LogTag)) { return; diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index 4c4f59068..7e9169f6a 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -13,22 +13,30 @@ 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 - internal SentryStructuredLogger _structuredLogger = 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) + { + _loggerFactory = loggerFactory; + } + public void Register(IHub hub, SentryOptions sentryOptions) { _hub = hub; // This should never happen, but if it does... _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options is not of type 'SentryUnityOptions'."); - _structuredLogger = Sentry.SentrySdk.Logger; + _structuredLogger = _loggerFactory?.Invoke() ?? Sentry.SentrySdk.Logger; // If called twice (i.e. init with the same options object) the integration will reference itself as the // original handler loghandler and endlessly forward to itself @@ -97,11 +105,12 @@ public void LogFormat(LogType logType, UnityEngine.Object? context, string forma private void ProcessLog(LogType logType, UnityEngine.Object? context, string format, params object[] args) { - if (_hub?.IsEnabled is not true) + if (_hub?.IsEnabled is not true || !_options.Experimental.EnableLogs) { return; } + // We're not capturing the SDK's own logs if (args.Length > 1 && args[0] is UnityLogger.LogTag) { return; diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index 4a5653069..06369f64f 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -16,11 +16,14 @@ private class Fixture { public TestHub Hub { get; set; } = null!; public SentryUnityOptions SentryOptions { get; set; } = null!; + public TestStructuredLogger? StructuredLogger { get; set; } public UnityLogHandlerIntegration GetSut() { var application = new TestApplication(); - var integration = new UnityLogHandlerIntegration(application); + var integration = StructuredLogger != null + ? new UnityLogHandlerIntegration(application, () => StructuredLogger) + : new UnityLogHandlerIntegration(application); integration.Register(Hub, SentryOptions); return integration; } @@ -81,15 +84,14 @@ public void Register_RegisteredASecondTime_LogsWarningAndReturns() public void ProcessException_ExperimentalCaptureEnabled_CapturesStructuredLog() { _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Exception] = true; + _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); - var testStructuredLogger = new TestStructuredLogger(); - sut._structuredLogger = testStructuredLogger; var message = TestContext.CurrentContext.Test.Name; sut.ProcessException(new Exception(message), null); - Assert.AreEqual(1, testStructuredLogger.LogCalls.Count); - var logCall = testStructuredLogger.LogCalls.Single(); + Assert.AreEqual(1, _fixture.StructuredLogger.LogCalls.Count); + var logCall = _fixture.StructuredLogger.LogCalls.Single(); Assert.AreEqual("Error", logCall.level); Assert.AreEqual(message, logCall.message); } @@ -98,23 +100,21 @@ public void ProcessException_ExperimentalCaptureEnabled_CapturesStructuredLog() public void ProcessException_ExperimentalCaptureDisabled_DoesNotCaptureStructuredLog() { _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Exception] = false; + _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); - var testStructuredLogger = new TestStructuredLogger(); - sut._structuredLogger = testStructuredLogger; var message = TestContext.CurrentContext.Test.Name; sut.ProcessException(new Exception(message), null); - Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + Assert.AreEqual(0, _fixture.StructuredLogger.LogCalls.Count); } [Test] public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() { _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Error] = true; + _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); - var testStructuredLogger = new TestStructuredLogger(); - sut._structuredLogger = testStructuredLogger; const string? format = "{0}: {1}"; const string? message = "Test message"; @@ -122,7 +122,7 @@ public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() sut.LogFormat(LogType.Error, null, format, UnityLogger.LogTag, message); - Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + Assert.AreEqual(0, _fixture.StructuredLogger.LogCalls.Count); } [Test] @@ -137,9 +137,8 @@ public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() public void LogFormat_WithExperimentalFlag_CapturesStructuredLogWhenEnabled(LogType logType, string expectedLevel, bool captureEnabled) { _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[logType] = captureEnabled; + _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); - var testStructuredLogger = new TestStructuredLogger(); - sut._structuredLogger = testStructuredLogger; var message = TestContext.CurrentContext.Test.Name; LogAssert.Expect(logType, message); @@ -148,14 +147,14 @@ public void LogFormat_WithExperimentalFlag_CapturesStructuredLogWhenEnabled(LogT if (captureEnabled) { - Assert.AreEqual(1, testStructuredLogger.LogCalls.Count); - var logCall = testStructuredLogger.LogCalls.Single(); + Assert.AreEqual(1, _fixture.StructuredLogger.LogCalls.Count); + var logCall = _fixture.StructuredLogger.LogCalls.Single(); Assert.AreEqual(expectedLevel, logCall.level); Assert.AreEqual(message, logCall.message); } else { - Assert.AreEqual(0, testStructuredLogger.LogCalls.Count); + Assert.AreEqual(0, _fixture.StructuredLogger.LogCalls.Count); } } } From 5facd7656ac824f50f59b3ca431c0a138b06d502 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 14:17:21 +0200 Subject: [PATCH 14/18] Missed enable check and test --- .../UnityLogHandlerIntegration.cs | 4 ++-- .../UnityLogHandlerIntegrationTests.cs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index 7e9169f6a..745c08e51 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -110,8 +110,8 @@ private void ProcessLog(LogType logType, UnityEngine.Object? context, string for return; } - // We're not capturing the SDK's own logs - if (args.Length > 1 && args[0] is UnityLogger.LogTag) + // We're not capturing the SDK's own logs. + if (args.Length > 1 && Equals(args[0], UnityLogger.LogTag)) { return; } diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index 06369f64f..facf88c51 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -83,6 +83,7 @@ public void Register_RegisteredASecondTime_LogsWarningAndReturns() [Test] public void ProcessException_ExperimentalCaptureEnabled_CapturesStructuredLog() { + _fixture.SentryOptions.Experimental.EnableLogs = true; _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Exception] = true; _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); @@ -112,6 +113,7 @@ public void ProcessException_ExperimentalCaptureDisabled_DoesNotCaptureStructure [Test] public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() { + _fixture.SentryOptions.Experimental.EnableLogs = true; _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Error] = true; _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); @@ -125,6 +127,22 @@ public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() Assert.AreEqual(0, _fixture.StructuredLogger.LogCalls.Count); } + [Test] + public void LogFormat_WithEnableLogsFalse_DoesNotCaptureStructuredLog() + { + _fixture.SentryOptions.Experimental.EnableLogs = false; + _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[LogType.Error] = true; + _fixture.StructuredLogger = new TestStructuredLogger(); + var sut = _fixture.GetSut(); + var message = TestContext.CurrentContext.Test.Name; + + LogAssert.Expect(LogType.Error, message); + + sut.LogFormat(LogType.Error, null, message); + + Assert.AreEqual(0, _fixture.StructuredLogger.LogCalls.Count); + } + [Test] [TestCase(LogType.Log, "Info", true)] [TestCase(LogType.Log, "Info", false)] @@ -136,6 +154,7 @@ public void LogFormat_WithSentryLogTag_DoesNotCaptureStructuredLog() [TestCase(LogType.Assert, "Error", false)] public void LogFormat_WithExperimentalFlag_CapturesStructuredLogWhenEnabled(LogType logType, string expectedLevel, bool captureEnabled) { + _fixture.SentryOptions.Experimental.EnableLogs = true; _fixture.SentryOptions.Experimental.CaptureStructuredLogsForLogType[logType] = captureEnabled; _fixture.StructuredLogger = new TestStructuredLogger(); var sut = _fixture.GetSut(); From 5eaec84c1af22a714702717a854403ec432c19db Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 14:37:54 +0200 Subject: [PATCH 15/18] Bump global.json --- global.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 9cd89532c..f260fed21 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.304", - "workloadVersion": "9.0.304", + "version": "10.0.100-rc.2.25502.107", + "workloadVersion": "10.0.100-rc.2.25513.4", "rollForward": "disable", "allowPrerelease": false } From 590cf2e4d0f59a472fffbc51e77f69bbdd861dfe Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 24 Oct 2025 14:42:35 +0200 Subject: [PATCH 16/18] Renamed scriptable options --- .../Resources/Sentry/SentryOptions.asset | 12 +++++------ .../ConfigurationWindow/LoggingTab.cs | 20 +++++++++---------- .../ScriptableSentryUnityOptions.cs | 20 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset index 17acd40a1..069914f26 100644 --- a/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset +++ b/samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset @@ -36,11 +36,11 @@ MonoBehaviour: k__BackingField: 20 k__BackingField: 10 k__BackingField: 1 - k__BackingField: 0 - k__BackingField: 1 - k__BackingField: 1 - k__BackingField: 1 - k__BackingField: 1 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 1 k__BackingField: 0 k__BackingField: 1 k__BackingField: 1 @@ -57,7 +57,7 @@ MonoBehaviour: k__BackingField: 1 k__BackingField: 2000 k__BackingField: 30 - k__BackingField: 1 + k__BackingField: 0 k__BackingField: 5000 k__BackingField: 1 k__BackingField: f401000057020000 diff --git a/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs b/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs index 12cc02c7f..4a33285f3 100644 --- a/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs +++ b/src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs @@ -18,26 +18,26 @@ internal static void Display(ScriptableSentryUnityOptions options) EditorGUI.indentLevel++; - options.OnDebugLog = EditorGUILayout.Toggle( + options.StructuredLogOnDebugLog = EditorGUILayout.Toggle( new GUIContent("Debug.Log", "Whether the SDK should forward Debug.Log messages to Sentry structured logging"), - options.OnDebugLog); - options.OnDebugLogWarning = EditorGUILayout.Toggle( + options.StructuredLogOnDebugLog); + options.StructuredLogOnDebugLogWarning = EditorGUILayout.Toggle( new GUIContent("Debug.LogWarning", "Whether the SDK should forward Debug.LogWarning messages to Sentry structured logging"), - options.OnDebugLogWarning); - options.OnDebugLogAssertion = EditorGUILayout.Toggle( + options.StructuredLogOnDebugLogWarning); + options.StructuredLogOnDebugLogAssertion = EditorGUILayout.Toggle( new GUIContent("Debug.LogAssertion", "Whether the SDK should forward Debug.LogAssertion messages to Sentry structured logging"), - options.OnDebugLogAssertion); - options.OnDebugLogError = EditorGUILayout.Toggle( + options.StructuredLogOnDebugLogAssertion); + options.StructuredLogOnDebugLogError = EditorGUILayout.Toggle( new GUIContent("Debug.LogError", "Whether the SDK should forward Debug.LogError messages to Sentry structured logging"), - options.OnDebugLogError); - options.OnDebugLogException = EditorGUILayout.Toggle( + options.StructuredLogOnDebugLogError); + options.StructuredLogOnDebugLogException = EditorGUILayout.Toggle( new GUIContent("Debug.LogException", "Whether the SDK should forward Debug.LogException messages to Sentry structured logging"), - options.OnDebugLogException); + options.StructuredLogOnDebugLogException); EditorGUI.indentLevel--; EditorGUILayout.EndToggleGroup(); diff --git a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs index 3d6b0366b..7f1a60f26 100644 --- a/src/Sentry.Unity/ScriptableSentryUnityOptions.cs +++ b/src/Sentry.Unity/ScriptableSentryUnityOptions.cs @@ -59,11 +59,11 @@ public static string GetConfigPath(string? notDefaultConfigName = null) [field: SerializeField] public int MaxViewHierarchyDepth { get; set; } = 10; [field: SerializeField] public bool EnableStructuredLogging { get; set; } = false; - [field: SerializeField] public bool OnDebugLog { get; set; } = false; - [field: SerializeField] public bool OnDebugLogWarning { get; set; } = true; - [field: SerializeField] public bool OnDebugLogAssertion { get; set; } = true; - [field: SerializeField] public bool OnDebugLogError { get; set; } = true; - [field: SerializeField] public bool OnDebugLogException { get; set; } = true; + [field: SerializeField] public bool StructuredLogOnDebugLog { get; set; } = false; + [field: SerializeField] public bool StructuredLogOnDebugLogWarning { get; set; } = true; + [field: SerializeField] public bool StructuredLogOnDebugLogAssertion { get; set; } = true; + [field: SerializeField] public bool StructuredLogOnDebugLogError { get; set; } = true; + [field: SerializeField] public bool StructuredLogOnDebugLogException { get; set; } = true; [field: SerializeField] public bool AttachBreadcrumbsToEvents { get; set; } = false; @@ -198,11 +198,11 @@ internal SentryUnityOptions ToSentryUnityOptions( EnableLogs = EnableStructuredLogging, CaptureStructuredLogsForLogType = { - [LogType.Log] = OnDebugLog, - [LogType.Warning] = OnDebugLogWarning, - [LogType.Assert] = OnDebugLogAssertion, - [LogType.Error] = OnDebugLogError, - [LogType.Exception] = OnDebugLogException + [LogType.Log] = StructuredLogOnDebugLog, + [LogType.Warning] = StructuredLogOnDebugLogWarning, + [LogType.Assert] = StructuredLogOnDebugLogAssertion, + [LogType.Error] = StructuredLogOnDebugLogError, + [LogType.Exception] = StructuredLogOnDebugLogException }, AttachBreadcrumbsToEvents = AttachBreadcrumbsToEvents } From 78c80adff96716895098e698a90de28d57d50c20 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 28 Oct 2025 17:02:11 +0100 Subject: [PATCH 17/18] Use the logger on the hub --- .../Integrations/UnityApplicationLoggingIntegration.cs | 2 +- src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs index 8b4460e97..7c222aaf9 100644 --- a/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs @@ -32,7 +32,7 @@ public void Register(IHub hub, SentryOptions sentryOptions) { _hub = hub; // This should never throw - _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options passed is not of type SentryUnityOptions"); + _options = sentryOptions as SentryUnityOptions ?? throw new ArgumentException("Options is not of type 'SentryUnityOptions'."); _logTimeDebounce = new LogTimeDebounce(_options.DebounceTimeLog); _warningTimeDebounce = new WarningTimeDebounce(_options.DebounceTimeWarning); diff --git a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs index 745c08e51..d869a4ceb 100644 --- a/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs +++ b/src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs @@ -35,8 +35,8 @@ public void Register(IHub hub, SentryOptions sentryOptions) { _hub = hub; // This should never happen, but if it does... - _options = sentryOptions as SentryUnityOptions ?? throw new InvalidOperationException("Options is not of type 'SentryUnityOptions'."); - _structuredLogger = _loggerFactory?.Invoke() ?? Sentry.SentrySdk.Logger; + _options = sentryOptions as SentryUnityOptions ?? throw new ArgumentException("Options is not of type 'SentryUnityOptions'."); + _structuredLogger = _loggerFactory?.Invoke() ?? _hub.Logger; // If called twice (i.e. init with the same options object) the integration will reference itself as the // original handler loghandler and endlessly forward to itself From 61060331663b273299eeb31d6948a0e7bced7103 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 29 Oct 2025 10:22:56 +0100 Subject: [PATCH 18/18] Fixed no-logger-fallback --- test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs index facf88c51..7edff354c 100644 --- a/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs +++ b/test/Sentry.Unity.Tests/UnityLogHandlerIntegrationTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using NUnit.Framework; +using Sentry.Internal; using Sentry.Protocol; using Sentry.Unity.Integrations; using Sentry.Unity.Tests.SharedClasses; @@ -23,7 +24,7 @@ public UnityLogHandlerIntegration GetSut() var application = new TestApplication(); var integration = StructuredLogger != null ? new UnityLogHandlerIntegration(application, () => StructuredLogger) - : new UnityLogHandlerIntegration(application); + : new UnityLogHandlerIntegration(application, () => DisabledSentryStructuredLogger.Instance); integration.Register(Hub, SentryOptions); return integration; } @@ -45,7 +46,7 @@ public void SetUp() public void CaptureException_ExceptionCapturedAndMechanismSet() { var sut = _fixture.GetSut(); - var message = NUnit.Framework.TestContext.CurrentContext.Test.Name; + var message = "test message" + Guid.NewGuid(); sut.ProcessException(new Exception(message), null);