Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions src/Sentry.Unity.Editor/ConfigurationWindow/LoggingTab.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,26 @@ internal static void Display(ScriptableSentryUnityOptions options)

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.StructuredLogOnDebugLog = EditorGUILayout.Toggle(
new GUIContent("Debug.Log",
"Whether the SDK should forward Debug.Log messages to Sentry structured logging"),
options.StructuredLogOnDebugLog);
options.StructuredLogOnDebugLogWarning = EditorGUILayout.Toggle(
new GUIContent("Debug.LogWarning",
"Whether the SDK should forward Debug.LogWarning messages to Sentry structured logging"),
options.StructuredLogOnDebugLogWarning);
options.StructuredLogOnDebugLogAssertion = EditorGUILayout.Toggle(
new GUIContent("Debug.LogAssertion",
"Whether the SDK should forward Debug.LogAssertion messages to Sentry structured logging"),
options.StructuredLogOnDebugLogAssertion);
options.StructuredLogOnDebugLogError = EditorGUILayout.Toggle(
new GUIContent("Debug.LogError",
"Whether the SDK should forward Debug.LogError messages to Sentry structured logging"),
options.StructuredLogOnDebugLogError);
options.StructuredLogOnDebugLogException = EditorGUILayout.Toggle(
new GUIContent("Debug.LogException",
"Whether the SDK should forward Debug.LogException messages to Sentry structured logging"),
options.StructuredLogOnDebugLogException);

EditorGUI.indentLevel--;
EditorGUILayout.EndToggleGroup();
Expand Down
76 changes: 76 additions & 0 deletions src/Sentry.Unity/ContentDebounce.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using UnityEngine;

namespace Sentry.Unity;

/// <summary>
/// Interface for log message deduplication.
/// </summary>
public interface IUnityLogMessageDebounce
{
/// <summary>
/// Checks if a log message should be debounced based on its content.
/// Returns true if the message should be allowed through, false if it should be blocked.
/// </summary>
bool Debounced(string message, string stacktrace, LogType logType);
}

/// <summary>
/// Content-based debounce that deduplicates log messages based on their content hash.
/// This class is not thread-safe and is designed to be called by Unity non-threaded logger callback.
/// </summary>
public class ContentDebounce : IUnityLogMessageDebounce
{
private static DateTimeOffset Now => DateTimeOffset.UtcNow;

private readonly struct LogEntry
{
public readonly int Hash;
public readonly DateTimeOffset Timestamp;

public LogEntry(int hash, DateTimeOffset timestamp)
{
Hash = hash;
Timestamp = timestamp;
}
}

private readonly TimeSpan _debounceWindow;
private readonly LogEntry[] _ringBuffer;
private int _head;

public ContentDebounce(TimeSpan debounceWindow, int bufferSize = 100)
{
_debounceWindow = debounceWindow;
_ringBuffer = new LogEntry[bufferSize];
_head = 0;
}

/// <summary>
/// Checks if the log content should be debounced.
/// Returns true if the message should be allowed through, false if it should be blocked.
/// </summary>
public bool Debounced(string message, string stacktrace, LogType logType)
{
var contentHash = HashCode.Combine(message, stacktrace);
var currentTime = Now;

foreach (var entry in _ringBuffer)
{
if (entry.Hash != contentHash || entry.Timestamp == default)
{
continue;
}

var timeSinceLastSeen = currentTime - entry.Timestamp;
if (timeSinceLastSeen < _debounceWindow)
{
return false;
}
}

_ringBuffer[_head] = new LogEntry(contentHash, currentTime);
_head = (_head + 1) % _ringBuffer.Length;
return true;
}
}
1 change: 0 additions & 1 deletion src/Sentry.Unity/Integrations/LifeCycleIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public void Register(IHub hub, SentryOptions sentryOptions)
_options.DiagnosticLogger?.LogDebug("Resuming session.");
hub.ResumeSession();
};

_sentryMonoBehaviour.ApplicationPausing += () =>
{
if (!hub.IsEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ internal class UnityApplicationLoggingIntegration : ISdkIntegration
private readonly IApplication _application;
private readonly ISystemClock _clock;

private ErrorTimeDebounce _errorTimeDebounce = null!; // Set in Register
private LogTimeDebounce _logTimeDebounce = null!; // Set in Register
private WarningTimeDebounce _warningTimeDebounce = null!; // Set in Register

private IHub _hub = null!; // Set in Register
private SentryUnityOptions _options = null!; // Set in Register

Expand All @@ -35,10 +31,6 @@ public void Register(IHub hub, SentryOptions sentryOptions)
_hub = hub ?? throw new ArgumentException("Hub is null.");
_options = sentryOptions as SentryUnityOptions ?? throw new ArgumentException("Options is not of type 'SentryUnityOptions'.");

_logTimeDebounce = new LogTimeDebounce(_options.DebounceTimeLog);
_warningTimeDebounce = new WarningTimeDebounce(_options.DebounceTimeWarning);
_errorTimeDebounce = new ErrorTimeDebounce(_options.DebounceTimeError);

_application.LogMessageReceived += OnLogMessageReceived;
_application.Quitting += OnQuitting;
}
Expand All @@ -51,32 +43,25 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo
return;
}

if (IsGettingDebounced(logType))
if (IsGettingDebounced(message, stacktrace, logType))
{
_options.LogDebug("Log message of type '{0}' is getting debounced.", logType);
return;
}

ProcessError(message, stacktrace, logType);
ProcessBreadcrumbs(message, logType);
ProcessStructuredLog(message, logType);
}

private bool IsGettingDebounced(LogType logType)
private bool IsGettingDebounced(string message, string stacktrace, LogType logType)
{
if (_options.EnableLogDebouncing is false)
{
return false;
}

return logType switch
{
LogType.Exception => !_errorTimeDebounce.Debounced(),
LogType.Error or LogType.Assert => !_errorTimeDebounce.Debounced(),
LogType.Log => !_logTimeDebounce.Debounced(),
LogType.Warning => !_warningTimeDebounce.Debounced(),
_ => true
};
// Use the debouncer from options - returns true if allowed, false if blocked
return !_options.LogDebouncer.Debounced(message, stacktrace, logType);
}

private void ProcessError(string message, string stacktrace, LogType logType)
Expand All @@ -91,11 +76,11 @@ private void ProcessError(string message, string stacktrace, LogType logType)
if (_options.AttachStacktrace && !string.IsNullOrEmpty(stacktrace))
{
var evt = UnityLogEventFactory.CreateMessageEvent(message, stacktrace, SentryLevel.Error, _options);
_hub.CaptureEvent(evt);
_hub?.CaptureEvent(evt);
}
else
{
_hub.CaptureMessage(message, level: SentryLevel.Error);
_hub?.CaptureMessage(message, level: SentryLevel.Error);
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/Sentry.Unity/Integrations/UnityLogHandlerIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal sealed class UnityLogHandlerIntegration : ISdkIntegration, ILogHandler
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 void Register(IHub hub, SentryOptions sentryOptions)
{
Expand Down Expand Up @@ -61,6 +62,12 @@ internal void ProcessException(Exception exception, UnityEngine.Object? context)
// https://docs.sentry.io/platforms/unity/troubleshooting/#unhandled-exceptions---debuglogexception
exception.SetSentryMechanism("Unity.LogException", handled: false, terminal: false);
_ = _hub.CaptureException(exception);

if (_options.CaptureStructuredLogsForLogType.TryGetValue(LogType.Exception, out var captureException) && captureException)
{
_options.LogDebug("Capturing structured log message of type '{0}'.", LogType.Exception);
_structuredLogger.LogError(exception.Message);
}
}

public void LogFormat(LogType logType, UnityEngine.Object? context, string format, params object[] args)
Expand Down
12 changes: 5 additions & 7 deletions src/Sentry.Unity/Integrations/UnityWebGLExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ internal sealed class UnityWebGLExceptionHandler : ISdkIntegration
private readonly IApplication _application;
private IHub _hub = null!;
private SentryUnityOptions _options = null!;
private ErrorTimeDebounce _errorTimeDebounce = null!;

internal UnityWebGLExceptionHandler(IApplication? application = null)
{
Expand All @@ -27,7 +26,6 @@ public void Register(IHub hub, SentryOptions sentryOptions)
_options = sentryOptions as SentryUnityOptions
?? throw new ArgumentException("Options is not of type 'SentryUnityOptions'.");

_errorTimeDebounce = new ErrorTimeDebounce(_options.DebounceTimeError);
_application.LogMessageReceived += OnLogMessageReceived;
_application.Quitting += OnQuitting;
}
Expand All @@ -50,11 +48,11 @@ internal void OnLogMessageReceived(string message, string stacktrace, LogType lo
return;
}

if (_options.EnableLogDebouncing && !_errorTimeDebounce.Debounced())
{
_options.LogDebug("Exception is getting debounced.");
return;
}
// if (_options.EnableLogDebouncing && !_errorTimeDebounce.Debounced())
// {
// _options.LogDebug("Exception is getting debounced.");
// return;
// }

_options.LogDebug("Capturing exception on WebGL through LogMessageReceived.");
var evt = UnityLogEventFactory.CreateExceptionEvent(message, stacktrace, false, _options);
Expand Down
8 changes: 2 additions & 6 deletions src/Sentry.Unity/ScriptableSentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ public static string GetConfigPath(string? notDefaultConfigName = null)
[field: SerializeField] public bool CaptureInEditor { get; set; } = true;

[field: SerializeField] public bool EnableLogDebouncing { get; set; } = false;
[field: SerializeField] public int DebounceTimeLog { get; set; } = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
[field: SerializeField] public int DebounceTimeWarning { get; set; } = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
[field: SerializeField] public int DebounceTimeError { get; set; } = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;
[field: SerializeField] public int DebounceTimeWindow { get; set; } = (int)TimeSpan.FromSeconds(1).TotalMilliseconds;

[field: SerializeField] public double TracesSampleRate { get; set; } = 0;
[field: SerializeField] public bool AutoStartupTraces { get; set; } = true;
Expand Down Expand Up @@ -149,9 +147,7 @@ internal SentryUnityOptions ToSentryUnityOptions(
Dsn = Dsn,
CaptureInEditor = CaptureInEditor,
EnableLogDebouncing = EnableLogDebouncing,
DebounceTimeLog = TimeSpan.FromMilliseconds(DebounceTimeLog),
DebounceTimeWarning = TimeSpan.FromMilliseconds(DebounceTimeWarning),
DebounceTimeError = TimeSpan.FromMilliseconds(DebounceTimeError),
DebounceTimeWindow = TimeSpan.FromMilliseconds(DebounceTimeWindow),
TracesSampleRate = TracesSampleRate,
AutoStartupTraces = AutoStartupTraces,
AutoSceneLoadTraces = AutoSceneLoadTraces,
Expand Down
23 changes: 14 additions & 9 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,29 @@ public sealed class SentryUnityOptions : SentryOptions
public bool CaptureInEditor { get; set; } = true;

/// <summary>
/// Whether Sentry events should be debounced it too frequent.
/// Whether Sentry events should be deduplicated if they occur too frequently.
/// When enabled, duplicate log messages with the same content will be suppressed within the debounce window.
/// </summary>
public bool EnableLogDebouncing { get; set; } = false;

/// <summary>
/// Timespan between sending events of LogType.Log
/// Time window for deduplicating log messages with identical content.
/// If the same log message appears multiple times within this window, only the first occurrence is captured.
/// </summary>
public TimeSpan DebounceTimeLog { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan DebounceTimeWindow { get; set; } = TimeSpan.FromSeconds(1);

/// <summary>
/// Timespan between sending events of LogType.Warning
/// </summary>
public TimeSpan DebounceTimeWarning { get; set; } = TimeSpan.FromSeconds(1);
private IUnityLogMessageDebounce? _logDebouncer;

/// <summary>
/// Timespan between sending events of LogType.Assert, LogType.Exception and LogType.Error
/// The debouncer used for deduplicating log messages.
/// Defaults to ContentDebounce which uses content-based hashing.
/// Can be set to a custom implementation for advanced deduplication logic.
/// </summary>
public TimeSpan DebounceTimeError { get; set; } = TimeSpan.FromSeconds(1);
public IUnityLogMessageDebounce LogDebouncer
{
get => _logDebouncer ??= new ContentDebounce(DebounceTimeWindow);
set => _logDebouncer = value;
}


private CompressionLevelWithAuto _requestBodyCompressionLevel = CompressionLevelWithAuto.Auto;
Expand Down
55 changes: 0 additions & 55 deletions src/Sentry.Unity/TimeDebounceBase.cs

This file was deleted.

Loading