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 ;
2022-05-11 13:51:56 +08:00
using System.Linq ;
2019-07-30 12:30:26 +08:00
using System.Net ;
2022-05-17 11:53:13 +08:00
using osu.Framework ;
2022-05-11 13:03:16 +08:00
using osu.Framework.Allocation ;
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-11 13:52:08 +08:00
using osu.Framework.Statistics ;
2022-05-11 13:11:20 +08:00
using osu.Game.Beatmaps ;
2022-05-11 13:03:16 +08:00
using osu.Game.Configuration ;
2022-05-11 13:51:56 +08:00
using osu.Game.Database ;
using osu.Game.Models ;
2022-05-10 13:12:31 +08:00
using osu.Game.Online.API.Requests.Responses ;
2022-05-11 13:03:16 +08:00
using osu.Game.Overlays ;
2022-05-16 14:47:00 +08:00
using osu.Game.Rulesets ;
2022-05-11 13:51:56 +08:00
using osu.Game.Skinning ;
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 ;
2022-05-11 13:03:16 +08:00
private readonly OsuGame game ;
2019-11-12 21:12:38 +08:00
public SentryLogger ( OsuGame game )
2018-08-03 18:25:55 +08:00
{
2022-05-11 13:03:16 +08:00
this . game = game ;
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 ;
2022-06-01 23:14:07 +08:00
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
2022-06-02 00:12:29 +08:00
options . Release = $"osu@{game.Version.Replace($@" - { OsuGameBase . BUILD_SUFFIX } ", string.Empty)}" ;
2022-05-10 13:44:54 +08:00
} ) ;
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-11 13:03:16 +08:00
} , scope = >
{
2022-05-11 13:51:56 +08:00
var beatmap = game . Dependencies . Get < IBindable < WorkingBeatmap > > ( ) . Value . BeatmapInfo ;
2022-05-16 14:47:00 +08:00
var ruleset = game . Dependencies . Get < IBindable < RulesetInfo > > ( ) . Value ;
2022-05-11 13:51:56 +08:00
2022-05-11 13:03:16 +08:00
scope . Contexts [ @"config" ] = new
{
Game = game . Dependencies . Get < OsuConfigManager > ( ) . GetLoggableState ( )
// TODO: add framework config here. needs some consideration on how to expose.
} ;
2022-05-11 13:11:20 +08:00
2022-05-11 13:51:56 +08:00
game . Dependencies . Get < RealmAccess > ( ) . Run ( realm = >
{
scope . Contexts [ @"realm" ] = new
{
Counts = new
{
BeatmapSets = realm . All < BeatmapSetInfo > ( ) . Count ( ) ,
Beatmaps = realm . All < BeatmapInfo > ( ) . Count ( ) ,
Files = realm . All < RealmFile > ( ) . Count ( ) ,
2022-05-16 15:07:56 +08:00
Rulesets = realm . All < RulesetInfo > ( ) . Count ( ) ,
RulesetsAvailable = realm . All < RulesetInfo > ( ) . Count ( r = > r . Available ) ,
2022-05-11 13:51:56 +08:00
Skins = realm . All < SkinInfo > ( ) . Count ( ) ,
}
} ;
} ) ;
2022-05-11 13:11:20 +08:00
2022-05-11 13:52:08 +08:00
scope . Contexts [ @"global statistics" ] = GlobalStatistics . GetStatistics ( )
. GroupBy ( s = > s . Group )
. ToDictionary ( g = > g . Key , items = > items . ToDictionary ( i = > i . Name , g = > g . DisplayValue ) ) ;
2022-05-11 13:11:20 +08:00
scope . Contexts [ @"beatmap" ] = new
{
Name = beatmap . ToString ( ) ,
2022-05-16 14:47:00 +08:00
Ruleset = beatmap . Ruleset . InstantiationInfo ,
2022-05-11 13:11:20 +08:00
beatmap . OnlineID ,
} ;
2022-05-16 14:47:00 +08:00
scope . Contexts [ @"ruleset" ] = new
{
2022-05-16 14:50:15 +08:00
ruleset . ShortName ,
2022-05-16 14:47:00 +08:00
ruleset . Name ,
ruleset . InstantiationInfo ,
ruleset . OnlineID
} ;
2022-05-11 13:03:16 +08:00
scope . Contexts [ @"clocks" ] = new
{
Audio = game . Dependencies . Get < MusicController > ( ) . CurrentTrack . CurrentTime ,
Game = game . Clock . CurrentTime ,
} ;
2022-05-16 14:50:15 +08:00
2022-06-03 13:21:35 +08:00
scope . SetTag ( @"beatmap" , $"{beatmap.OnlineID}" ) ;
2022-05-16 14:50:15 +08:00
scope . SetTag ( @"ruleset" , ruleset . ShortName ) ;
2022-05-17 11:53:13 +08:00
scope . SetTag ( @"os" , $"{RuntimeInfo.OS} ({Environment.OSVersion})" ) ;
scope . SetTag ( @"processor count" , Environment . ProcessorCount . ToString ( ) ) ;
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
}
}