Last active
December 14, 2015 07:09
-
-
Save aroben/5048415 to your computer and use it in GitHub Desktop.
How GitHub for Windows sends exceptions to Haystack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Central | |
class DotNetException | |
def self.report(app, params={}) | |
if $stats | |
if params['type'] == 'crash' | |
$stats.increment "#{app}.crashes" | |
else | |
$stats.increment "#{app}.exceptions" | |
end | |
end | |
exception_message = params.delete 'exception_message' | |
message = "#{params.delete 'log_message'}\n#{exception_message}".strip | |
params[:message] = message if message.length > 0 | |
if params[:backtrace] | |
frames = params[:backtrace].scan(/^\s+\S+\s+(\S+\(.*\))/).flatten | |
first_stack_frame = frames.find { |frame| frame =~ /^GitHub\./ } || frames.first | |
end | |
unless first_stack_frame | |
if params[:class] == 'System.Net.WebException' | |
# WebException's don't have a meaningful backtrace, so we use the | |
# exception message instead. | |
first_stack_frame = exception_message | |
else | |
first_stack_frame = 'unknown' | |
end | |
end | |
rollup_context = "#{params[:class]}\n#{first_stack_frame}" | |
rollup = Digest::MD5.hexdigest rollup_context | |
Failbot.report(Exception.new, params.merge(:rollup => rollup, :rollup_context => rollup_context)) | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.Reactive; | |
using System.Reactive.Linq; | |
using System.Reflection; | |
using GitHub.Api; | |
using GitHub.Extensions; | |
using GitHub.IO; | |
using NLog; | |
using NLog.Targets; | |
using ReactiveUI; | |
using RestSharp; | |
using LogManager = NLog.LogManager; | |
namespace GitHub.Helpers | |
{ | |
[Target("Haystack")] | |
public class HaystackTarget : Target | |
{ | |
public static readonly string IsFatalExceptionProperty = "IsFatalException"; | |
static readonly Logger log = LogManager.GetCurrentClassLogger(); | |
readonly IHaystackContext context; | |
readonly IRestClient restClient; | |
Exception lastException; | |
bool hasSeenFatalException; | |
// We get our exceptions into Haystack by way of Central. | |
public HaystackTarget(IHaystackContext context, IProgram program, IEnvironment environment) | |
: this(context, CreateRestClient(program, environment)) | |
{ | |
} | |
public HaystackTarget(IHaystackContext context, IRestClient restClient) | |
{ | |
Ensure.ArgumentNotNull(context, "context"); | |
Ensure.ArgumentNotNull(restClient, "restClient"); | |
this.context = context; | |
this.restClient = restClient; | |
} | |
static IRestClient CreateRestClient(IProgram program, IEnvironment environment) | |
{ | |
var client = new RestClient("https://central.github.com/") | |
{ | |
HttpFactory = new GitHubHttpFactory(program, environment) | |
}; | |
// We get our exceptions into Haystack by way of Central. | |
client.AddDefaultParameter("version", (Assembly.GetEntryAssembly() ?? typeof(HaystackTarget).Assembly).GetName().Version, ParameterType.GetOrPost); | |
client.AddDefaultParameter("culture", CultureInfo.CurrentCulture.Name, ParameterType.GetOrPost); | |
client.AddDefaultParameter("os_version", Environment.OSVersion.GetOperatingSystemVersionString(), ParameterType.GetOrPost); | |
return client; | |
} | |
protected override void Write(LogEventInfo logEvent) | |
{ | |
if (!IsFakeRestClient(restClient)) | |
{ | |
// If the rest client is real, we only want to log the exception if | |
// We're running the app for real in a production environment. | |
#if DEBUG | |
// Always return for DEBUG builds. We don't want to log our | |
// local builds. | |
return; | |
#else | |
if (RxApp.InUnitTestRunner()) return; | |
#endif | |
} | |
Debug.Assert(logEvent != null, "Somehow the logEvent is null"); | |
if (logEvent.Exception == null || logEvent.Exception == lastException || hasSeenFatalException || logEvent.LoggerName == log.Name) | |
return; | |
Debug.Assert(logEvent.Level != null, "Somehow the logEvent.Level is null"); | |
lastException = logEvent.Exception; | |
context.CurrentGitHubUserName | |
.Catch<string, Exception>(ex => | |
{ | |
log.ErrorException("Failed to get current GitHub username", ex); | |
return Observable.Return<string>(null); | |
}) | |
.Select(userName => | |
{ | |
var request = new RestRequest("/api/windows/exception", Method.POST); | |
request.AddParameter("desktop_path", | |
context.Environment.DesktopPath, | |
ParameterType.GetOrPost); | |
request.AddParameter("desktopdirectory_path", | |
context.Environment.DesktopDirectoryPath, | |
ParameterType.GetOrPost); | |
request.AddParameter("applicationdata_path", | |
context.Environment.ApplicationDataPath, | |
ParameterType.GetOrPost); | |
request.AddParameter("localgithubapplicationdata_path", | |
context.Environment.LocalGitHubApplicationDataPath, | |
ParameterType.GetOrPost); | |
request.AddParameter("userprofile_path", | |
context.Environment.UserProfilePath, | |
ParameterType.GetOrPost); | |
request.AddParameter("class", logEvent.Exception.InnermostException().GetType().FullName, ParameterType.GetOrPost); | |
request.AddParameter("logger_name", logEvent.LoggerName, ParameterType.GetOrPost); | |
request.AddParameter("log_level", logEvent.Level.Name, ParameterType.GetOrPost); | |
request.AddParameter("log_message", logEvent.FormattedMessage, ParameterType.GetOrPost); | |
request.AddParameter("exception_message", logEvent.Exception.InnermostException().VerboseMessage(), ParameterType.GetOrPost); | |
request.AddParameter("backtrace", logEvent.Exception.FullStackTrace(), ParameterType.GetOrPost); | |
try | |
{ | |
var repoNameWithOwner = context.SelectedRepositoryNameWithOwner; | |
if (repoNameWithOwner != null) | |
request.AddParameter("repo", repoNameWithOwner, ParameterType.GetOrPost); | |
} | |
catch (Exception ex) | |
{ | |
log.ErrorException("Failed to get current GitHub repository", ex); | |
// Oh well. We'll just send the report without a repo name. | |
} | |
string enterpriseHost = null; | |
try | |
{ | |
enterpriseHost = context.EnterpriseHost; | |
request.AddParameter("enterprise_host", enterpriseHost); | |
} | |
catch (Exception ex) | |
{ | |
log.ErrorException("Failed to determine enterprise host", ex); | |
// Oh well. We'll just send the report without that info. | |
} | |
if (!userName.IsNullOrEmpty()) | |
{ | |
if (!enterpriseHost.IsNullOrEmpty()) | |
userName = string.Format(CultureInfo.InvariantCulture, "{0}@{1}", userName, enterpriseHost); | |
request.AddParameter("user", userName, ParameterType.GetOrPost); | |
} | |
if (logEvent.Properties.ContainsKey(IsFatalExceptionProperty)) | |
{ | |
hasSeenFatalException = true; | |
request.AddParameter("type", "crash", ParameterType.GetOrPost); | |
request.Timeout = 2000; | |
restClient.Execute(request); | |
} | |
else | |
{ | |
restClient.ExecuteAsync(request, response => {}); | |
} | |
return Observable.Return(Unit.Default); | |
}) | |
.Subscribe(_ => {}, ex => | |
{ | |
try | |
{ | |
var message = string.Format(CultureInfo.InvariantCulture, "Failed to send exception to Haystack. Log event and exception we were trying to send:{0}{1}{0}{2}{0}{0}Exception that prevented sending it:", Environment.NewLine, logEvent, logEvent.Exception); | |
log.ErrorException(message, ex); | |
} | |
catch (Exception) | |
{ | |
// Now there's *really* nothing we can do. | |
} | |
}); | |
} | |
static bool IsFakeRestClient(IRestClient restClient) | |
{ | |
return restClient.GetType().Name.Contains("Proxy", StringComparison.OrdinalIgnoreCase); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment