diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0553c44a..de44e1d8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- Add HTTP request breadcrumb ([#1113](https://github.com/getsentry/sentry-dotnet/pull/1113)) +- Integration for Google Cloud Functions ([#1085](https://github.com/getsentry/sentry-dotnet/pull/1085)) - Add ClearAttachments to Scope ([#1104](https://github.com/getsentry/sentry-dotnet/pull/1104)) - Add additional logging and additional fallback for installation ID ([#1103](https://github.com/getsentry/sentry-dotnet/pull/1103)) diff --git a/README.md b/README.md index 4adb0202dc..2a26837d1a 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Sentry SDK for .NET | **Sentry.Extensions.Logging** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.Extensions.Logging.svg)](https://www.nuget.org/packages/Sentry.Extensions.Logging) | [![NuGet](https://img.shields.io/nuget/v/Sentry.Extensions.Logging.svg)](https://www.nuget.org/packages/Sentry.Extensions.Logging) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.Extensions.Logging.svg)](https://www.nuget.org/packages/Sentry.Extensions.Logging) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/extensions-logging/) | | **Sentry.AspNetCore** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.AspNetCore.svg)](https://www.nuget.org/packages/Sentry.AspNetCore) | [![NuGet](https://img.shields.io/nuget/v/Sentry.AspNetCore.svg)](https://www.nuget.org/packages/Sentry.AspNetCore) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.AspNetCore.svg)](https://www.nuget.org/packages/Sentry.AspNetCore) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/) | | **Sentry.AspNetCore.Grpc** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.AspNetCore.Grpc.svg)](https://www.nuget.org/packages/Sentry.AspNetCore.Grpc) | [![NuGet](https://img.shields.io/nuget/v/Sentry.AspNetCore.Grpc.svg)](https://www.nuget.org/packages/Sentry.AspNetCore.Grpc) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.AspNetCore.Grpc.svg)](https://www.nuget.org/packages/Sentry.AspNetCore.Grpc) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/) | +| **Sentry.Google.Cloud.Functions** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.Google.Cloud.Functions.svg)](https://www.nuget.org/packages/Sentry.Google.Cloud.Functions) | [![NuGet](https://img.shields.io/nuget/v/Sentry.Google.Cloud.Functions.svg)](https://www.nuget.org/packages/Sentry.Google.Cloud.Functions) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.Google.Cloud.Functions.svg)](https://www.nuget.org/packages/Sentry.Google.Cloud.Functions) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/gcp-functions/) | | **Sentry.AspNet** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.AspNet.svg)](https://www.nuget.org/packages/Sentry.AspNet) | [![NuGet](https://img.shields.io/nuget/v/Sentry.AspNet.svg)](https://www.nuget.org/packages/Sentry.AspNet) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.AspNet.svg)](https://www.nuget.org/packages/Sentry.AspNet) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/aspnet) | | **Sentry.EntityFramework** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.EntityFramework.svg)](https://www.nuget.org/packages/Sentry.EntityFramework) | [![NuGet](https://img.shields.io/nuget/v/Sentry.EntityFramework.svg)](https://www.nuget.org/packages/Sentry.EntityFramework) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.EntityFramework.svg)](https://www.nuget.org/packages/Sentry.EntityFramework) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/entityframework) | | **Sentry.Serilog** | [![Downloads](https://img.shields.io/nuget/dt/Sentry.Serilog.svg)](https://www.nuget.org/packages/Serilog) | [![NuGet](https://img.shields.io/nuget/v/Sentry.Serilog.svg)](https://www.nuget.org/packages/Sentry.Serilog) | [![NuGet](https://img.shields.io/nuget/vpre/Sentry.Serilog.svg)](https://www.nuget.org/packages/Sentry.Serilog) | [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/dotnet/guides/serilog) | diff --git a/Sentry.sln b/Sentry.sln index d7aa0ae5bd..b73bd9ddee 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -125,6 +125,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.EntityFramework.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.EntityFramework", "samples\Sentry.Samples.EntityFramework\Sentry.Samples.EntityFramework.csproj", "{8E4BA4C7-413C-4668-8F41-32F484FFB7AA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Google.Cloud.Functions", "samples\Sentry.Samples.Google.Cloud.Functions\Sentry.Samples.Google.Cloud.Functions.csproj", "{88269A52-A0BA-41B2-8DF3-505B66B17243}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions", "src\Sentry.Google.Cloud.Functions\Sentry.Google.Cloud.Functions.csproj", "{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions.Tests", "test\Sentry.Google.Cloud.Functions.Tests\Sentry.Google.Cloud.Functions.Tests.csproj", "{066522A4-8380-4D29-8DD0-973B1EDF0B39}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -271,6 +277,18 @@ Global {8E4BA4C7-413C-4668-8F41-32F484FFB7AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E4BA4C7-413C-4668-8F41-32F484FFB7AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E4BA4C7-413C-4668-8F41-32F484FFB7AA}.Release|Any CPU.Build.0 = Release|Any CPU + {88269A52-A0BA-41B2-8DF3-505B66B17243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88269A52-A0BA-41B2-8DF3-505B66B17243}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88269A52-A0BA-41B2-8DF3-505B66B17243}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88269A52-A0BA-41B2-8DF3-505B66B17243}.Release|Any CPU.Build.0 = Release|Any CPU + {D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}.Release|Any CPU.Build.0 = Release|Any CPU + {066522A4-8380-4D29-8DD0-973B1EDF0B39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {066522A4-8380-4D29-8DD0-973B1EDF0B39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -314,6 +332,9 @@ Global {8B38F62E-0DD5-486F-96F5-2025AFB9B491} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB} {840B220E-68EC-4ECB-AEA1-67B0151F17FC} = {83263231-1A2A-4733-B759-EEFF14E8C5D5} {8E4BA4C7-413C-4668-8F41-32F484FFB7AA} = {77454495-55EE-4B40-A089-71B9E8F82E89} + {88269A52-A0BA-41B2-8DF3-505B66B17243} = {77454495-55EE-4B40-A089-71B9E8F82E89} + {D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB} + {066522A4-8380-4D29-8DD0-973B1EDF0B39} = {83263231-1A2A-4733-B759-EEFF14E8C5D5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0C652B1A-DF72-4EE5-A98B-194FE2C054F6} diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/.assets/gcp_sample.png b/samples/Sentry.Samples.Google.Cloud.Functions/.assets/gcp_sample.png new file mode 100644 index 0000000000..8c7130e525 Binary files /dev/null and b/samples/Sentry.Samples.Google.Cloud.Functions/.assets/gcp_sample.png differ diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/Function.cs b/samples/Sentry.Samples.Google.Cloud.Functions/Function.cs new file mode 100644 index 0000000000..85af6b716d --- /dev/null +++ b/samples/Sentry.Samples.Google.Cloud.Functions/Function.cs @@ -0,0 +1,20 @@ +using System; +using Google.Cloud.Functions.Framework; +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using Google.Cloud.Functions.Hosting; +using Microsoft.Extensions.Logging; + +[assembly: FunctionsStartup(typeof(SentryStartup))] + +public class Function : IHttpFunction +{ + private readonly ILogger _logger; + public Function(ILogger logger) => _logger = logger; + + public Task HandleAsync(HttpContext context) + { + _logger.LogInformation("Useful info that is added to the breadcrumb list."); + throw new Exception("Bad function"); + } +} diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/README.md b/samples/Sentry.Samples.Google.Cloud.Functions/README.md new file mode 100644 index 0000000000..8c41fd42b0 --- /dev/null +++ b/samples/Sentry.Samples.Google.Cloud.Functions/README.md @@ -0,0 +1,15 @@ +# Sample integration with Google Cloud Function + +Edit the `appsettings.json` file and add your own `DSN`. +You can get one at [sentry.io](sentry.io). + +Run this sample: + +```sh +dotnet run +``` + +Browse the URL: `http://127.0.0.1:8080/`. +An event will be sent to the DSN configured on `appsettings.json`. + +![Sample event in Sentry](.assets/gcp_sample.png) diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj b/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj new file mode 100644 index 0000000000..68498e6ad4 --- /dev/null +++ b/samples/Sentry.Samples.Google.Cloud.Functions/Sentry.Samples.Google.Cloud.Functions.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + PreserveNewest + + + + diff --git a/samples/Sentry.Samples.Google.Cloud.Functions/appsettings.json b/samples/Sentry.Samples.Google.Cloud.Functions/appsettings.json new file mode 100644 index 0000000000..47c5e8e5ba --- /dev/null +++ b/samples/Sentry.Samples.Google.Cloud.Functions/appsettings.json @@ -0,0 +1,7 @@ +{ + "Sentry": { + "Dsn": "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537", + "IncludeRequestPayload": true, + "SendDefaultPii": true + } +} diff --git a/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs b/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs index 06c6da8309..d53ebc3af3 100644 --- a/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs +++ b/src/Sentry.AspNetCore.Grpc/SentryBuilderExtensions.cs @@ -29,7 +29,6 @@ public static ISentryBuilder AddGrpc(this ISentryBuilder builder) return builder; } - private class SentryGrpcEventProcessor : ISentryEventProcessor { private static readonly SdkVersion NameAndVersion diff --git a/src/Sentry.AspNetCore/Extensions/DependencyInjection/ServiceCollectionExtensions.cs b/src/Sentry.AspNetCore/Extensions/DependencyInjection/ServiceCollectionExtensions.cs index f1be9f1510..7fa5d047bc 100644 --- a/src/Sentry.AspNetCore/Extensions/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Sentry.AspNetCore/Extensions/DependencyInjection/ServiceCollectionExtensions.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection /// Extension methods for /// [EditorBrowsable(EditorBrowsableState.Never)] - internal static class ServiceCollectionExtensions + public static class ServiceCollectionExtensions { /// /// Adds Sentry's services to the diff --git a/src/Sentry.AspNetCore/Properties/AssemblyInfo.cs b/src/Sentry.AspNetCore/Properties/AssemblyInfo.cs index 6a278072df..9d6c85d316 100644 --- a/src/Sentry.AspNetCore/Properties/AssemblyInfo.cs +++ b/src/Sentry.AspNetCore/Properties/AssemblyInfo.cs @@ -2,4 +2,5 @@ [assembly: InternalsVisibleTo("Sentry.AspNetCore.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("Sentry.AspNetCore.Grpc, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] +[assembly: InternalsVisibleTo("Sentry.Google.Cloud.Functions.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010059964a931488bcdbd14657f1ee0df32df61b57b3d14d7290c262c2cc9ddaad6ec984044f761f778e1823049d2cb996a4f58c8ea5b46c37891414cb34b4036b1c178d7b582289d2eef3c0f1e9b692c229a306831ee3d371d9e883f0eb0f74aeac6c6ab8c85fd1ec04b267e15a31532c4b4e2191f5980459db4dce0081f1050fb8")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Sentry.AspNetCore/SentryAspNetCoreLoggerProvider.cs b/src/Sentry.AspNetCore/SentryAspNetCoreLoggerProvider.cs index 5a032faa6f..c7dd778855 100644 --- a/src/Sentry.AspNetCore/SentryAspNetCoreLoggerProvider.cs +++ b/src/Sentry.AspNetCore/SentryAspNetCoreLoggerProvider.cs @@ -4,9 +4,15 @@ namespace Sentry.AspNetCore { + /// + /// Logger provider for Sentry. + /// [ProviderAlias("Sentry")] - internal class SentryAspNetCoreLoggerProvider : SentryLoggerProvider + public class SentryAspNetCoreLoggerProvider : SentryLoggerProvider { + /// + /// Creates a new instance of + /// public SentryAspNetCoreLoggerProvider(IOptions options, IHub hub) : base(options, hub) { diff --git a/src/Sentry.AspNetCore/SentryAspNetCoreOptionsSetup.cs b/src/Sentry.AspNetCore/SentryAspNetCoreOptionsSetup.cs index 96e3b61ed5..6d77295b47 100644 --- a/src/Sentry.AspNetCore/SentryAspNetCoreOptionsSetup.cs +++ b/src/Sentry.AspNetCore/SentryAspNetCoreOptionsSetup.cs @@ -13,16 +13,25 @@ namespace Sentry.AspNetCore { - internal class SentryAspNetCoreOptionsSetup : ConfigureFromConfigurationOptions + /// + /// Sets up ASP.NET Core option for Sentry. + /// + public class SentryAspNetCoreOptionsSetup : ConfigureFromConfigurationOptions { private readonly IHostingEnvironment _hostingEnvironment; + /// + /// Creates a new instance of . + /// public SentryAspNetCoreOptionsSetup( ILoggerProviderConfiguration providerConfiguration, IHostingEnvironment hostingEnvironment) : base(providerConfiguration.Configuration) => _hostingEnvironment = hostingEnvironment; + /// + /// Configures the . + /// public override void Configure(SentryAspNetCoreOptions options) { base.Configure(options); diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index 23ab0aa2c6..1c23658402 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -4,9 +4,14 @@ namespace Sentry.AspNetCore { - /// - internal class SentryStartupFilter : IStartupFilter + /// + /// Starts Sentry integration. + /// + public class SentryStartupFilter : IStartupFilter { + /// + /// Adds Sentry to the pipeline. + /// public Action Configure(Action next) => e => { e.UseSentry(); diff --git a/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj b/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj new file mode 100644 index 0000000000..4563c26e81 --- /dev/null +++ b/src/Sentry.Google.Cloud.Functions/Sentry.Google.Cloud.Functions.csproj @@ -0,0 +1,14 @@ + + + + net5.0;netcoreapp3.1 + $(PackageTags);GCP;Google Cloud Functions + Official Google Cloud Functions integration for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time. + + + + + + + + diff --git a/src/Sentry.Google.Cloud.Functions/SentryStartup.cs b/src/Sentry.Google.Cloud.Functions/SentryStartup.cs new file mode 100644 index 0000000000..dac18f9453 --- /dev/null +++ b/src/Sentry.Google.Cloud.Functions/SentryStartup.cs @@ -0,0 +1,82 @@ +using Google.Cloud.Functions.Hosting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Sentry; +using Sentry.AspNetCore; +using Sentry.Extensibility; +using Sentry.Reflection; + +namespace Google.Cloud.Functions.Framework +{ + /// + /// Starts up the GCP Function integration. + /// + public class SentryStartup : FunctionsStartup + { + /// + /// Configure Sentry logging. + /// + public override void ConfigureLogging(WebHostBuilderContext context, ILoggingBuilder logging) + { + base.ConfigureLogging(context, logging); + logging.AddConfiguration(context.Configuration); + + logging.Services.AddSingleton(); + + // TODO: refactor this with SentryWebHostBuilderExtensions + var section = context.Configuration.GetSection("Sentry"); + logging.Services.Configure(section); + + logging.Services.Configure(options => + { + // Make sure all events are flushed out + options.FlushOnCompletedRequest = true; + }); + + logging.Services.AddSingleton, SentryAspNetCoreOptionsSetup>(); + logging.Services.AddSingleton(); + + logging.AddFilter( + "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware", + LogLevel.None); + + logging.Services.AddSentry(); + } + + /// + /// Configure Sentry services. + /// + /// + /// + public override void ConfigureServices(WebHostBuilderContext context, IServiceCollection services) + { + base.ConfigureServices(context, services); + services.AddTransient(); + } + + private class SentryGoogleCloudFunctionEventProcessor : ISentryEventProcessor + { + private static readonly SdkVersion NameAndVersion + = typeof(SentryStartup).Assembly.GetNameAndVersion(); + + private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name; + private const string SdkName = "sentry.dotnet.google-cloud-function"; + + public SentryEvent Process(SentryEvent @event) + { + // Take over the SDK name since this wraps ASP.NET Core + @event.Sdk.Name = SdkName; + @event.Sdk.Version = NameAndVersion.Version; + + if (NameAndVersion.Version != null) + { + @event.Sdk.AddPackage(ProtocolPackageName, NameAndVersion.Version); + } + + return @event; + } + } + } +} diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index ef356d59b3..b9f1813433 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -61,18 +62,29 @@ protected override async Task SendAsync( // in case the user didn't set an inner handler. InnerHandler ??= new HttpClientHandler(); + var requestMethod = request.Method.Method.ToUpperInvariant(); + var url = request.RequestUri?.ToString() ?? string.Empty; + // Start a span that tracks this request // (may be null if transaction is not set on the scope) var span = _hub.GetSpan()?.StartChild( "http.client", // e.g. "GET https://example.com" - $"{request.Method.Method.ToUpperInvariant()} {request.RequestUri}" + $"{requestMethod} {url}" ); try { var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + var breadcrumbData = new Dictionary + { + { "url", url }, + { "method", requestMethod }, + { "status_code", ((int)response.StatusCode).ToString() } + }; + _hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData); + // This will handle unsuccessful status codes as well span?.Finish( SpanStatusConverter.FromHttpStatusCode(response.StatusCode) diff --git a/test/Sentry.Google.Cloud.Functions.Tests/IntegrationTests.cs b/test/Sentry.Google.Cloud.Functions.Tests/IntegrationTests.cs new file mode 100644 index 0000000000..e1018fa17d --- /dev/null +++ b/test/Sentry.Google.Cloud.Functions.Tests/IntegrationTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Google.Cloud.Functions.Framework; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Sentry.AspNetCore; +using Xunit; + +namespace Sentry.Google.Cloud.Functions.Tests +{ + public class IntegrationTests + { + public static string ExpectedMessage = Guid.NewGuid().ToString(); + + [Fact] + public async Task SentryIntegrationTest_CaptureUnhandledException() + { + var evt = new ManualResetEventSlim(); + + var requests = new List(); + void Verify(HttpRequestMessage message) + { + requests.Add(message.Content.ReadAsStringAsync().Result); + evt.Set(); + } + + var host = Host.CreateDefaultBuilder() + .ConfigureWebHost(webHostBuilder => webHostBuilder + .ConfigureServices((context, services) => + { + services.Configure(o => + { + // So we can assert on the payload without the need to Gzip decompress + o.RequestBodyCompressionLevel = CompressionLevel.NoCompression; + o.CreateHttpClientHandler = () => new TestHandler(Verify); + }); + services.AddFunctionTarget(); + }) + // Based on: https://github.com/GoogleCloudPlatform/functions-framework-dotnet/blob/a8a34526053c40e84ff096a43b1d357ea4d3be6c/src/Google.Cloud.Functions.Hosting.Tests/FunctionsStartupTest.cs#L117 + .UseFunctionsStartup(new SentryStartup()) + .Configure((context, app) => app.UseFunctionsFramework(context)) + .ConfigureAppConfiguration(c => c.AddInMemoryCollection(new [] + { + new KeyValuePair("Sentry:Dsn", "https://key@sentry.io/project") + })) + .UseTestServer()) + .Build(); + + await host.StartAsync(); + + using var testServer = host.GetTestServer(); + using var client = testServer.CreateClient(); + try + { + using var response = await client.GetAsync("/"); + } + catch (Exception e) when (e.Message == ExpectedMessage) + { + // Synchronizing in the tests because `OnCompleted` is not being called with TestServer. + Assert.True(evt.Wait(TimeSpan.FromSeconds(3))); + Assert.True(requests.Any(p => p.Contains(ExpectedMessage)), + "Expected error to be captured"); + Assert.True(requests.All(p => p.Contains("sentry.dotnet.google-cloud-function")), + "Expected SDK name to be in the payload"); + return; // pass + } + Assert.False(true, "Exception should bubble from Middleware"); + } + + public class FailingFunction : IHttpFunction + { + public Task HandleAsync(HttpContext context) => throw new Exception(ExpectedMessage); + } + + private class TestHandler : HttpClientHandler + { + private readonly Action _messageCallback; + + public TestHandler(Action messageCallback) => _messageCallback = messageCallback; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + _messageCallback(request); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + } + } +} diff --git a/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj b/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj new file mode 100644 index 0000000000..2da8763eac --- /dev/null +++ b/test/Sentry.Google.Cloud.Functions.Tests/Sentry.Google.Cloud.Functions.Tests.csproj @@ -0,0 +1,12 @@ + + + + net5.0;netcoreapp3.1 + + + + + + + + diff --git a/test/Sentry.Google.Cloud.Functions.Tests/SentryStartupTests.cs b/test/Sentry.Google.Cloud.Functions.Tests/SentryStartupTests.cs new file mode 100644 index 0000000000..d7d7f83abf --- /dev/null +++ b/test/Sentry.Google.Cloud.Functions.Tests/SentryStartupTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Google.Cloud.Functions.Framework; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sentry.AspNetCore; +using Xunit; + +namespace Sentry.Google.Cloud.Functions.Tests +{ + public class SentryStartupTests + { + public IWebHostEnvironment HostingEnvironment { get; set; } = Substitute.For(); + public WebHostBuilderContext WebHostBuilderContext { get; set; } + + public ILoggingBuilder LoggingBuilder { get; set; } + + public SentryStartupTests() + { + WebHostBuilderContext = new WebHostBuilderContext + { + Configuration = Substitute.For(), + HostingEnvironment = HostingEnvironment + }; + LoggingBuilder = new TestLoggingBuilder(); + LoggingBuilder.Services.AddSingleton(HostingEnvironment); + + } + + private class TestLoggingBuilder : ILoggingBuilder + { + public IServiceCollection Services { get; } = new ServiceCollection(); + } + + [Fact] + public void ConfigureLogging_SentryAspNetCoreOptions_FlushOnCompletedRequestTrue() + { + var sut = new SentryStartup(); + sut.ConfigureLogging(WebHostBuilderContext, LoggingBuilder); + + var provider = LoggingBuilder.Services.BuildServiceProvider(); + var option = provider.GetRequiredService>(); + Assert.True(option.Value.FlushOnCompletedRequest); + } + + [Theory, MemberData(nameof(ExpectedServices))] + public void UseSentry_Parameterless_ServicesRegistered(Action assert) + { + var sut = new SentryStartup(); + sut.ConfigureLogging(WebHostBuilderContext, LoggingBuilder); + + var provider = LoggingBuilder.Services.BuildServiceProvider(); + var option = provider.GetRequiredService>(); + assert(LoggingBuilder.Services); + } + + public static IEnumerable ExpectedServices() + { + yield return new object[] { + new Action(c => + Assert.Single(c, d => d.ServiceType == typeof(IHub)))}; + yield return new object[] { + new Action(c => + Assert.Single(c, d => d.ImplementationType == typeof(SentryAspNetCoreLoggerProvider)))}; + yield return new object[] { + new Action(c => + Assert.Single(c, d => d.ImplementationType == typeof(SentryAspNetCoreOptionsSetup)))}; + yield return new object[] { + new Action(c => + Assert.Single(c, d => d.ImplementationType == typeof(AspNetCoreEventProcessor)))}; + } + } +} diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index 478cb369b2..131a3a5c5c 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -120,5 +121,48 @@ public async Task SendAsync_ExceptionThrown_ExceptionLinkedToSpan() // Assert hub.Received(1).BindException(exception, Arg.Any()); // second argument is an implicitly created span } + + [Fact] + public async Task SendAsync_Executed_BreadcrumbCreated() + { + // Arrange + var scope = new Scope(); + var hub = Substitute.For(); + hub.When(h => h.ConfigureScope(Arg.Any>())) + .Do(c => c.Arg>()(scope)); + + var url = "https://example.com/"; + + var urlKey = "url"; + var methodKey = "method"; + var statusKey = "status_code"; + var expectedBreadcrumbData = new Dictionary + { + { urlKey, url }, + { methodKey, "GET" }, + { statusKey, "200" } + }; + var expectedType = "http"; + var expectedCategory = "http"; + using var sentryHandler = new SentryHttpMessageHandler(hub); + using var client = new HttpClient(sentryHandler); + + // Act + await client.GetAsync(url); + var BreadcrumbGenerated = scope.Breadcrumbs.First(); + + // Assert + Assert.Equal(expectedType, BreadcrumbGenerated.Type); + Assert.Equal(expectedCategory, BreadcrumbGenerated.Category); + + Assert.True(BreadcrumbGenerated.Data.ContainsKey(urlKey)); + Assert.Equal(expectedBreadcrumbData[urlKey], BreadcrumbGenerated.Data[urlKey]); + + Assert.True(BreadcrumbGenerated.Data.ContainsKey(methodKey)); + Assert.Equal(expectedBreadcrumbData[methodKey], BreadcrumbGenerated.Data[methodKey]); + + Assert.True(BreadcrumbGenerated.Data.ContainsKey(statusKey)); + Assert.Equal(expectedBreadcrumbData[statusKey], BreadcrumbGenerated.Data[statusKey]); + } } }