2019-01-24 16:43:03 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-08-03 18:25:55 +08:00
2022-05-10 13:25:10 +08:00
#nullable enable
2018-08-03 18:25:55 +08:00
using System ;
2022-05-10 13:25:10 +08:00
using System.Diagnostics ;
2018-10-31 15:43:35 +08:00
using System.IO ;
2019-07-30 12:30:26 +08:00
using System.Net ;
2022-05-10 13:12:31 +08:00
using osu.Framework.Bindables ;
2018-08-03 18:25:55 +08:00
using osu.Framework.Logging ;
2022-05-10 13:12:31 +08:00
using osu.Game.Online.API.Requests.Responses ;
2019-11-12 21:12:38 +08:00
using Sentry ;
2022-05-10 14:07:02 +08:00
using Sentry.Protocol ;
2018-08-03 18:25:55 +08:00
namespace osu.Game.Utils
{
/// <summary>
/// Report errors to sentry.
/// </summary>
2019-11-12 21:12:38 +08:00
public class SentryLogger : IDisposable
2018-08-03 18:25:55 +08:00
{
2022-05-10 13:25:10 +08:00
private IBindable < APIUser > ? localUser ;
2022-05-10 13:12:31 +08:00
2022-05-10 13:44:54 +08:00
private readonly IDisposable ? sentrySession ;
2019-11-12 21:12:38 +08:00
public SentryLogger ( OsuGame game )
2018-08-03 18:25:55 +08:00
{
2022-05-10 13:44:54 +08:00
sentrySession = SentrySdk . Init ( options = >
2019-11-12 21:12:38 +08:00
{
2022-05-10 13:44:54 +08:00
// Not setting the dsn will completely disable sentry.
if ( game . IsDeployedBuild )
options . Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2" ;
2019-11-21 21:55:31 +08:00
2022-05-10 13:44:54 +08:00
options . AutoSessionTracking = true ;
options . IsEnvironmentUser = false ;
options . Release = game . Version ;
} ) ;
2019-11-12 22:16:48 +08:00
2022-01-18 10:27:28 +08:00
Logger . NewEntry + = processLogEntry ;
2022-05-10 13:25:10 +08:00
}
2022-05-10 13:45:55 +08:00
~ SentryLogger ( ) = > Dispose ( false ) ;
2022-05-10 13:25:10 +08:00
public void AttachUser ( IBindable < APIUser > user )
{
Debug . Assert ( localUser = = null ) ;
2022-05-10 13:12:31 +08:00
2022-05-10 13:25:10 +08:00
localUser = user . GetBoundCopy ( ) ;
localUser . BindValueChanged ( u = >
2022-05-10 13:12:31 +08:00
{
2022-05-10 13:44:54 +08:00
SentrySdk . ConfigureScope ( scope = > scope . User = new User
2022-05-10 13:12:31 +08:00
{
2022-05-10 13:25:10 +08:00
Username = u . NewValue . Username ,
Id = u . NewValue . Id . ToString ( ) ,
2022-05-10 13:44:54 +08:00
} ) ;
2022-05-10 13:25:10 +08:00
} , true ) ;
2022-01-18 10:27:28 +08:00
}
2019-03-08 11:00:12 +08:00
2022-01-18 10:27:28 +08:00
private void processLogEntry ( LogEntry entry )
{
if ( entry . Level < LogLevel . Verbose ) return ;
2018-08-03 18:25:55 +08:00
2022-01-18 10:27:28 +08:00
var exception = entry . Exception ;
2018-08-17 11:03:31 +08:00
2022-01-18 10:27:28 +08:00
if ( exception ! = null )
{
if ( ! shouldSubmitException ( exception ) ) return ;
2019-07-30 12:30:26 +08:00
2022-05-10 14:07:02 +08:00
// framework does some weird exception redirection which means sentry does not see unhandled exceptions using its automatic methods.
// but all unhandled exceptions still arrive via this pathway. we just need to mark them as unhandled for tagging purposes.
// easiest solution is to check the message matches what the framework logs this as.
// see https://github.com/ppy/osu-framework/blob/f932f8df053f0011d755c95ad9a2ed61b94d136b/osu.Framework/Platform/GameHost.cs#L336
2022-05-10 14:19:43 +08:00
bool wasUnhandled = entry . Message = = @"An unhandled error has occurred." ;
bool wasUnobserved = entry . Message = = @"An unobserved error has occurred." ;
if ( wasUnobserved )
{
// see https://github.com/getsentry/sentry-dotnet/blob/c6a660b1affc894441c63df2695a995701671744/src/Sentry/Integrations/TaskUnobservedTaskExceptionIntegration.cs#L39
exception . Data [ Mechanism . MechanismKey ] = @"UnobservedTaskException" ;
}
if ( wasUnhandled )
{
// see https://github.com/getsentry/sentry-dotnet/blob/main/src/Sentry/Integrations/AppDomainUnhandledExceptionIntegration.cs#L38-L39
exception . Data [ Mechanism . MechanismKey ] = @"AppDomain.UnhandledException" ;
}
exception . Data [ Mechanism . HandledKey ] = ! wasUnhandled ;
2022-05-10 14:07:02 +08:00
2022-05-10 13:44:54 +08:00
SentrySdk . CaptureEvent ( new SentryEvent ( exception )
2022-05-10 13:08:42 +08:00
{
Message = entry . Message ,
Level = getSentryLevel ( entry . Level ) ,
2022-05-10 13:44:54 +08:00
} ) ;
2022-01-18 10:27:28 +08:00
}
else
2022-05-10 15:14:04 +08:00
SentrySdk . AddBreadcrumb ( entry . Message , entry . Target . ToString ( ) , "navigation" , level : getBreadcrumbLevel ( entry . Level ) ) ;
2018-08-03 18:25:55 +08:00
}
2022-05-10 15:14:04 +08:00
private BreadcrumbLevel getBreadcrumbLevel ( LogLevel entryLevel )
{
switch ( entryLevel )
{
case LogLevel . Debug :
return BreadcrumbLevel . Debug ;
case LogLevel . Verbose :
return BreadcrumbLevel . Info ;
case LogLevel . Important :
return BreadcrumbLevel . Warning ;
case LogLevel . Error :
return BreadcrumbLevel . Error ;
default :
throw new ArgumentOutOfRangeException ( nameof ( entryLevel ) , entryLevel , null ) ;
}
}
private SentryLevel getSentryLevel ( LogLevel entryLevel )
2022-05-10 13:08:42 +08:00
{
switch ( entryLevel )
{
case LogLevel . Debug :
return SentryLevel . Debug ;
case LogLevel . Verbose :
return SentryLevel . Info ;
case LogLevel . Important :
return SentryLevel . Warning ;
case LogLevel . Error :
return SentryLevel . Error ;
default :
throw new ArgumentOutOfRangeException ( nameof ( entryLevel ) , entryLevel , null ) ;
}
}
2019-07-30 16:52:06 +08:00
private bool shouldSubmitException ( Exception exception )
{
switch ( exception )
{
case IOException ioe :
// disk full exceptions, see https://stackoverflow.com/a/9294382
const int hr_error_handle_disk_full = unchecked ( ( int ) 0x80070027 ) ;
const int hr_error_disk_full = unchecked ( ( int ) 0x80070070 ) ;
if ( ioe . HResult = = hr_error_handle_disk_full | | ioe . HResult = = hr_error_disk_full )
return false ;
break ;
case WebException we :
switch ( we . Status )
{
// more statuses may need to be blocked as we come across them.
case WebExceptionStatus . Timeout :
return false ;
}
break ;
}
return true ;
}
2018-08-03 18:25:55 +08:00
#region Disposal
public void Dispose ( )
{
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
protected virtual void Dispose ( bool isDisposing )
{
2022-01-18 10:27:28 +08:00
Logger . NewEntry - = processLogEntry ;
2022-05-10 13:44:54 +08:00
sentrySession ? . Dispose ( ) ;
2018-08-03 18:25:55 +08:00
}
#endregion
}
}