Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a Sentry event for failed HTTP requests #2320

Merged
merged 13 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .editorconfig
mattjohnsonpint marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,7 @@ dotnet_diagnostic.RCS1118.severity = silent
dotnet_diagnostic.RCS1080.severity = silent

# Declare type inside namespace
dotnet_diagnostic.RCS1110.severity = silent
dotnet_diagnostic.RCS1110.severity = silent
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:warning
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- Create a Sentry event for failed HTTP requests ([#2320](https://github.com/getsentry/sentry-dotnet/pull/2320))
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved

### Features

Expand Down
1 change: 0 additions & 1 deletion src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env
.SerializeToStringAsync(_options.DiagnosticLogger, _clock, cancellationToken).ConfigureAwait(false);
_options.LogDebug("Failed envelope '{0}' has payload:\n{1}\n", eventId, payload);


// SDK is in debug mode, and envelope was too large. To help troubleshoot:
const string persistLargeEnvelopePathEnvVar = "SENTRY_KEEP_LARGE_ENVELOPE_PATH";
if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/HttpHeadersExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sentry;
internal static class HttpHeadersExtensions
{
internal static string GetCookies(this HttpHeaders headers)
{
if (headers.TryGetValues("Cookie", out var values))
{
return string.Join("; ", values);
}
return string.Empty;
}
}
47 changes: 47 additions & 0 deletions src/Sentry/HttpStatusCodeRange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
namespace Sentry;

public readonly record struct HttpStatusCodeRange
{
public int Start { get; init; }

public int End { get; init; }

public HttpStatusCodeRange(int statusCode)
{
Start = statusCode;
End = statusCode;
}

public HttpStatusCodeRange(int start, int end)
{
if (start > end)
{
throw new ArgumentOutOfRangeException(nameof(start), "Range start must be after range end");
}

Start = start;
End = end;
}

public static implicit operator HttpStatusCodeRange((int Start, int End) range) => new(range.Start, range.End);

public static implicit operator HttpStatusCodeRange(int statusCode)
{
return new HttpStatusCodeRange(statusCode);
}

public static implicit operator HttpStatusCodeRange(HttpStatusCode statusCode)
{
return new HttpStatusCodeRange((int)statusCode);
}

public static implicit operator HttpStatusCodeRange((HttpStatusCode start, HttpStatusCode end) range)
{
return new HttpStatusCodeRange((int)range.start, (int)range.end);
}

public bool Contains(int statusCode)
=> statusCode >= Start && statusCode <= End;

public bool Contains(HttpStatusCode statusCode) => Contains((int)statusCode);
}
7 changes: 7 additions & 0 deletions src/Sentry/ISentryFailedRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sentry
{
internal interface ISentryFailedRequestHandler
{
void HandleResponse(HttpResponseMessage response);
}
}
11 changes: 11 additions & 0 deletions src/Sentry/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ public sealed class Request : IJsonSerializable
/// <value>The other.</value>
public IDictionary<string, string> Other => InternalOther ??= new Dictionary<string, string>();

internal void AddHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
{
foreach (var header in headers)
{
Headers.Add(
header.Key,
string.Join("; ", header.Value)
);
}
}

/// <summary>
/// Clones this instance.
/// </summary>
Expand Down
124 changes: 124 additions & 0 deletions src/Sentry/ResponseContext.cs
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using Sentry.Extensibility;
using Sentry.Internal.Extensions;

namespace Sentry;

/// <summary>
/// Sentry Response context interface.
/// </summary>
/// <example>
///{
/// "contexts": {
/// "response": {
/// "cookies": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
/// "headers": {
/// "content-type": "text/html"
/// /// ...
/// },
/// "status_code": 500,
/// "body_size": 1000, // in bytes
/// }
/// }
///}
/// </example>
/// <see href="https://develop.sentry.dev/sdk/event-payloads/types/#responsecontext"/>
public sealed class ResponseContext : IJsonSerializable
{
internal Dictionary<string, string>? InternalHeaders { get; set; }

/// <summary>
/// Gets or sets the HTTP response body size.
/// </summary>
/// <value>The request URL.</value>
public long? BodySize { get; set; }

/// <summary>
/// Gets or sets (optional) cookie values
/// </summary>
/// <value>The other.</value>
public string? Cookies { get; set; }

/// <summary>
/// Gets or sets the headers.
/// </summary>
/// <remarks>
/// If a header appears multiple times it needs to be merged according to the HTTP standard for header merging.
/// </remarks>
/// <value>The headers.</value>
public IDictionary<string, string> Headers => InternalHeaders ??= new Dictionary<string, string>();

/// <summary>
/// Gets or sets the HTTP Status response code
/// </summary>
/// <value>The HTTP method.</value>
public short? StatusCode { get; set; }

internal void AddHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
{
foreach (var header in headers)
{
Headers.Add(
header.Key,
string.Join("; ", header.Value)
);
}
}

/// <summary>
/// Clones this instance.
/// </summary>
public ResponseContext Clone()
{
var response = new ResponseContext();

CopyTo(response);

return response;
}

internal void CopyTo(ResponseContext? response)
{
if (response == null)
{
return;
}

response.BodySize ??= BodySize;
response.Cookies ??= Cookies;
response.StatusCode ??= StatusCode;

InternalHeaders?.TryCopyTo(response.Headers);
}

/// <inheritdoc />
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
{
writer.WriteStartObject();

writer.WriteNumberIfNotNull("body_size", BodySize);
writer.WriteStringIfNotWhiteSpace("cookies", Cookies);
writer.WriteStringDictionaryIfNotEmpty("headers", InternalHeaders!);
writer.WriteNumberIfNotNull("status_code", StatusCode);

writer.WriteEndObject();
}

/// <summary>
/// Parses from JSON.
/// </summary>
public static ResponseContext FromJson(JsonElement json)
{
var bodySize = json.GetPropertyOrNull("body_size")?.GetInt64();
var cookies = json.GetPropertyOrNull("cookies")?.GetString();
var headers = json.GetPropertyOrNull("headers")?.GetStringDictionaryOrNull();
var statusCode = json.GetPropertyOrNull("status_code")?.GetInt16();

return new ResponseContext
{
BodySize = bodySize,
Cookies = cookies,
InternalHeaders = headers?.WhereNotNullValue().ToDictionary(),
StatusCode = statusCode
};
}
}
95 changes: 95 additions & 0 deletions src/Sentry/SentryFailedRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Net;
using System.Reflection.PortableExecutable;
using Sentry.Internal;

namespace Sentry;

internal class SentryFailedRequestHandler : ISentryFailedRequestHandler
{
private readonly IHub _hub;
private readonly SentryOptions _options;

public const string ResponseKey = "response";
public const string MechanismType = "SentryFailedRequestHandler";

internal SentryFailedRequestHandler(IHub hub, SentryOptions options)
{
_hub = hub;
_options = options;
}

public void HandleResponse(HttpResponseMessage response)
{
// Ensure reponse and request are not null
if (response?.RequestMessage is null)
{
return;
}

// Don't capture if the option is disabled
if (_options?.CaptureFailedRequests is false)
{
return;
}

// Don't capture events for successful requets
if (_options?.FailedRequestStatusCodes.Any(range => range.Contains(response.StatusCode)) is false)
{
return;
}

// Ignore requests to the Sentry DSN
var uri = response.RequestMessage.RequestUri;
if (_options?.Dsn is { } dsn && new Uri(dsn).Host.Equals(uri?.Host, StringComparison.OrdinalIgnoreCase))
{
return;
}

// Ignore requests that don't match the FailedRequestTargets
var requestString = uri?.OriginalString ?? "";
if (_options?.FailedRequestTargets.ContainsMatch(requestString) is false)
{
return;
}

// Capture the event
try
{
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException exception)
{
exception.SetSentryMechanism(MechanismType);

var @event = new SentryEvent(exception);

var sentryRequest = new Request
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
{
Url = uri?.AbsoluteUri,
QueryString = uri?.Query,
Method = response.RequestMessage.Method.Method,
};
if (_options?.SendDefaultPii is true)
{
sentryRequest.Cookies = response.RequestMessage.Headers.GetCookies();
sentryRequest.AddHeaders(response.RequestMessage.Headers);
}

var responseContext = new ResponseContext
{
BodySize = response.Content?.Headers?.ContentLength ?? null,
StatusCode = (short)response.StatusCode
};
if (_options?.SendDefaultPii is true)
{
responseContext.Cookies = response.Headers.GetCookies();
responseContext.AddHeaders(response.Headers);
}

@event.Request = sentryRequest;
@event.Contexts[ResponseKey] = responseContext;

_hub.CaptureEvent(@event);
}
}
}
17 changes: 13 additions & 4 deletions src/Sentry/SentryHttpMessageHandler.cs
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class SentryHttpMessageHandler : DelegatingHandler
{
private readonly IHub _hub;
private readonly SentryOptions? _options;
private readonly ISentryFailedRequestHandler? _failedRequestHandler;

/// <summary>
/// Initializes an instance of <see cref="SentryHttpMessageHandler"/>.
Expand All @@ -18,12 +19,17 @@ public SentryHttpMessageHandler(IHub hub)
{
_hub = hub;
_options = hub.GetSentryOptions();
if (_options != null)
{
_failedRequestHandler = new SentryFailedRequestHandler(_hub, _options);
}
}

internal SentryHttpMessageHandler(IHub hub, SentryOptions options)
internal SentryHttpMessageHandler(IHub hub, SentryOptions options, ISentryFailedRequestHandler? failedRequestHandler = null)
{
_hub = hub;
_options = options;
_failedRequestHandler = failedRequestHandler;
}

/// <summary>
Expand All @@ -35,8 +41,8 @@ public SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub)
InnerHandler = innerHandler;
}

internal SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub, SentryOptions options)
: this(hub, options)
internal SentryHttpMessageHandler(HttpMessageHandler innerHandler, IHub hub, SentryOptions options, ISentryFailedRequestHandler? failedRequestHandler = null)
: this(hub, options, failedRequestHandler)
{
InnerHandler = innerHandler;
}
Expand Down Expand Up @@ -65,7 +71,7 @@ protected override async Task<HttpResponseMessage> SendAsync(
var requestMethod = request.Method.Method.ToUpperInvariant();
var url = request.RequestUri?.ToString() ?? string.Empty;

if (_options?.TracePropagationTargets.ShouldPropagateTrace(url) is true or null)
if (_options?.TracePropagationTargets.ContainsMatch(url) is true or null)
{
AddSentryTraceHeader(request);
AddBaggageHeader(request);
Expand All @@ -90,6 +96,9 @@ protected override async Task<HttpResponseMessage> SendAsync(
};
_hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);

// Create events for failed requests
_failedRequestHandler?.HandleResponse(response);

// This will handle unsuccessful status codes as well
span?.Finish(SpanStatusConverter.FromHttpStatusCode(response.StatusCode));

Expand Down
Loading