diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/IFaultExceptionHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/IFaultExceptionHandler.cs new file mode 100644 index 00000000000..fe971784a90 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/IFaultExceptionHandler.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.Telemetry; + +internal interface IFaultExceptionHandler +{ + bool HandleException(ITelemetryReporter reporter, Exception exception, string? message, object?[] @params); +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/ITelemetryReporter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/ITelemetryReporter.cs index 93d944c0e98..0d7b1e0827d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/ITelemetryReporter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/ITelemetryReporter.cs @@ -11,5 +11,5 @@ public interface ITelemetryReporter { void ReportEvent(string name, TelemetrySeverity severity); void ReportEvent(string name, TelemetrySeverity severity, ImmutableDictionary values); - void ReportFault(Exception exception, string? message, object[] @params); + void ReportFault(Exception exception, string? message, params object?[] @params); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/TelemetryReporter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/TelemetryReporter.cs index 5bfaaab88ef..152ddf7146f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/TelemetryReporter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.Common/Telemetry/TelemetryReporter.cs @@ -17,17 +17,21 @@ namespace Microsoft.AspNetCore.Razor.Telemetry; internal class TelemetryReporter : ITelemetryReporter { private readonly ImmutableArray _telemetrySessions; + private readonly IEnumerable _faultExceptionHandlers; private readonly ILogger? _logger; [ImportingConstructor] - public TelemetryReporter([Import(AllowDefault = true)] ILoggerFactory? loggerFactory = null) - : this(ImmutableArray.Create(TelemetryService.DefaultSession), loggerFactory) + public TelemetryReporter( + [Import(AllowDefault = true)] ILoggerFactory? loggerFactory = null, + [ImportMany] IEnumerable? faultExceptionHandlers = null) + : this(ImmutableArray.Create(TelemetryService.DefaultSession), loggerFactory, faultExceptionHandlers) { } - public TelemetryReporter(ImmutableArray telemetrySessions, ILoggerFactory? loggerFactory) + public TelemetryReporter(ImmutableArray telemetrySessions, ILoggerFactory? loggerFactory, IEnumerable? faultExceptionHandlers) { _telemetrySessions = telemetrySessions; + _faultExceptionHandlers = faultExceptionHandlers ?? Array.Empty(); _logger = loggerFactory?.CreateLogger(); } @@ -48,7 +52,7 @@ public void ReportEvent(string name, TelemetrySeverity severity, ImmutableDic Report(telemetryEvent); } - public void ReportFault(Exception exception, string? message, object[] @params) + public void ReportFault(Exception exception, string? message, params object?[] @params) { try { @@ -69,6 +73,24 @@ public void ReportFault(Exception exception, string? message, object[] @params) return; } + var handled = false; + foreach (var handler in _faultExceptionHandlers) + { + if (handler.HandleException(this, exception, message, @params)) + { + // This behavior means that each handler still gets a chance + // to respond to the exception. There's no real reason for this other + // than best guess. When it was added, there was only one handler but + // it was intended to be easy to add more. + handled = true; + } + } + + if (handled) + { + return; + } + var currentProcess = Process.GetCurrentProcess(); var faultEvent = new FaultEvent( @@ -78,6 +100,16 @@ public void ReportFault(Exception exception, string? message, object[] @params) exceptionObject: exception, gatherEventDetails: faultUtility => { + foreach (var data in @params) + { + if (data is null) + { + continue; + } + + faultUtility.AddErrorInformation(data.ToString()); + } + // Returning "0" signals that, if sampled, we should send data to Watson. // Any other value will cancel the Watson report. We never want to trigger a process dump manually, // we'll let TargetedNotifications determine if a dump should be collected. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp/Project/OmniSharpTelemetryReporter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp/Project/OmniSharpTelemetryReporter.cs index 28084a48cb9..c66aeb05358 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp/Project/OmniSharpTelemetryReporter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.OmniSharp/Project/OmniSharpTelemetryReporter.cs @@ -17,7 +17,7 @@ public void ReportEvent(string name, TelemetrySeverity severity, ImmutableDic { } - public void ReportFault(Exception exception, string? message, object[] @params) + public void ReportFault(Exception exception, string? message, params object?[] @params) { } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/JsonRPCFaultExceptionHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/JsonRPCFaultExceptionHandler.cs new file mode 100644 index 00000000000..3a843e73a08 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/JsonRPCFaultExceptionHandler.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Telemetry; +using StreamJsonRpc; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class JsonRPCFaultExceptionHandler : IFaultExceptionHandler +{ + public bool HandleException(ITelemetryReporter reporter, Exception exception, string? message, object?[] @params) + { + if (exception is not RemoteInvocationException remoteInvocationException) + { + return false; + } + + reporter.ReportFault(remoteInvocationException, remoteInvocationException.Message, remoteInvocationException.ErrorCode, remoteInvocationException.ErrorData, remoteInvocationException.DeserializedErrorData); + return true; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 7106d9b43e2..efa1042938c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -143,11 +143,16 @@ protected override ILspServices ConstructLspServices() // Folding Range Providers services.AddSingleton(); + services.AddSingleton(); + // Get the DefaultSession for telemetry. This is set by VS with // TelemetryService.SetDefaultSession and provides the correct // appinsights keys etc services.AddSingleton(provider => - new TelemetryReporter(ImmutableArray.Create(TelemetryService.DefaultSession), provider.GetRequiredService())); + new TelemetryReporter( + ImmutableArray.Create(TelemetryService.DefaultSession), + provider.GetRequiredService(), + provider.GetServices())); // Defaults: For when the caller hasn't provided them through the `configure` action. services.TryAddSingleton(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/NoOpTelemetryReporter.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/NoOpTelemetryReporter.cs index dee5f0b923c..6ead3eebcc2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/NoOpTelemetryReporter.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common/NoOpTelemetryReporter.cs @@ -24,7 +24,7 @@ public void ReportEvent(string name, TelemetrySeverity severity, ImmutableDic { } - public void ReportFault(Exception exception, string? message, object[] @params) + public void ReportFault(Exception exception, string? message, params object?[] @params) { } }