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 all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Initial work to support profiling in a future release. ([#2206](https://github.com/getsentry/sentry-dotnet/pull/2206))
- Create a Sentry event for failed HTTP requests ([#2320](https://github.com/getsentry/sentry-dotnet/pull/2320))
- Improve `WithScope` and add `WithScopeAsync` ([#2303](https://github.com/getsentry/sentry-dotnet/pull/2303)) ([#2309](https://github.com/getsentry/sentry-dotnet/pull/2309))
- Build .NET Standard 2.1 for Unity ([#2328](https://github.com/getsentry/sentry-dotnet/pull/2328))
- Add `RemoveExceptionFilter`, `RemoveEventProcessor` and `RemoveTransactionProcessor` extension methods on `SentryOptions` ([#2331](https://github.com/getsentry/sentry-dotnet/pull/2331))
Expand Down
9 changes: 9 additions & 0 deletions src/Sentry/Contexts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public sealed class Contexts : ConcurrentDictionary<string, object>, IJsonSerial
/// </remarks>
public OperatingSystem OperatingSystem => this.GetOrCreate<OperatingSystem>(OperatingSystem.Type);

/// <summary>
/// Response interface that contains information on any HTTP response related to the event.
/// </summary>
public Response Response => this.GetOrCreate<Response>(Response.Type);

/// <summary>
/// This describes a runtime in more detail.
/// </summary>
Expand Down Expand Up @@ -144,6 +149,10 @@ public static Contexts FromJson(JsonElement json)
{
result[name] = OperatingSystem.FromJson(value);
}
else if (string.Equals(type, Response.Type, StringComparison.OrdinalIgnoreCase))
{
result[name] = Response.FromJson(value);
}
else if (string.Equals(type, Runtime.Type, StringComparison.OrdinalIgnoreCase))
{
result[name] = Runtime.FromJson(value);
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
9 changes: 9 additions & 0 deletions src/Sentry/HttpHeadersExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sentry;

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

/// <summary>
/// Holds a fully-inclusive range of HTTP status codes.
/// e.g. Start = 500, End = 599 represents the range 500-599.
/// </summary>
public readonly record struct HttpStatusCodeRange
{
/// <summary>
/// The inclusive start of the range.
/// </summary>
public int Start { get; init; }

/// <summary>
/// The inclusive end of the range.
/// </summary>
public int End { get; init; }

/// <summary>
/// Creates a range that will only match a single value.
/// </summary>
/// <param name="statusCode">The value in the range.</param>
public HttpStatusCodeRange(int statusCode)
{
Start = statusCode;
End = statusCode;
}

/// <summary>
/// Creates a range that will match all values between <paramref name="start"/> and <paramref name="end"/>.
/// </summary>
/// <param name="start">The inclusive start of the range.</param>
/// <param name="end">The inclusive end of the range.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="start"/> is greater than <paramref name="end"/>.
/// </exception>
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;
}

/// <summary>
/// Implicitly converts a tuple of ints to a <see cref="HttpStatusCodeRange"/>.
/// </summary>
/// <param name="range">A tuple of ints to convert.</param>
public static implicit operator HttpStatusCodeRange((int Start, int End) range) => new(range.Start, range.End);

/// <summary>
/// Implicitly converts an int to a <see cref="HttpStatusCodeRange"/>.
/// </summary>
/// <param name="statusCode">An int to convert.</param>
public static implicit operator HttpStatusCodeRange(int statusCode)
{
return new HttpStatusCodeRange(statusCode);
}

/// <summary>
/// Implicitly converts an <see cref="HttpStatusCode"/> to a <see cref="HttpStatusCodeRange"/>.
/// </summary>
/// <param name="statusCode">A status code to convert.</param>
public static implicit operator HttpStatusCodeRange(HttpStatusCode statusCode)
{
return new HttpStatusCodeRange((int)statusCode);
}

/// <summary>
/// Implicitly converts a tuple of <see cref="HttpStatusCode"/> to a <see cref="HttpStatusCodeRange"/>.
/// </summary>
/// <param name="range">A tuple of status codes to convert.</param>
public static implicit operator HttpStatusCodeRange((HttpStatusCode start, HttpStatusCode end) range)
{
return new HttpStatusCodeRange((int)range.start, (int)range.end);
}

/// <summary>
/// Checks if a given status code is contained in the range.
/// </summary>
/// <param name="statusCode">Status code to check.</param>
/// <returns>True if the range contains the given status code.</returns>
public bool Contains(int statusCode)
=> statusCode >= Start && statusCode <= End;

/// <summary>
/// Checks if a given status code is contained in the range.
/// </summary>
/// <param name="statusCode">Status code to check.</param>
/// <returns>True if the range contains the given status code.</returns>
public bool Contains(HttpStatusCode statusCode) => Contains((int)statusCode);
}
6 changes: 6 additions & 0 deletions src/Sentry/ISentryFailedRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sentry;

internal interface ISentryFailedRequestHandler
{
void HandleResponse(HttpResponseMessage response);
}
138 changes: 138 additions & 0 deletions src/Sentry/Protocol/Response.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Internal.Extensions;

namespace Sentry.Protocol;

/// <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 Response : IJsonSerializable, ICloneable<Response>, IUpdatable<Response>
{
/// <summary>
/// Tells Sentry which type of context this is.
/// </summary>
public const string Type = "response";

internal Dictionary<string, string>? InternalHeaders { get; private set; }

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

/// <summary>
/// Gets or sets (optional) cookie values
/// </summary>
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>
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 Response Clone()
{
var response = new Response();

response.UpdateFrom(this);

return response;
}

/// <summary>
/// Updates this instance with data from the properties in the <paramref name="source"/>,
/// unless there is already a value in the existing property.
/// </summary>
public void UpdateFrom(Response source)
{
BodySize ??= source.BodySize;
Cookies ??= source.Cookies;
StatusCode ??= source.StatusCode;
source.InternalHeaders?.TryCopyTo(Headers);
}

/// <summary>
/// Updates this instance with data from the properties in the <paramref name="source"/>,
/// unless there is already a value in the existing property.
/// </summary>
public void UpdateFrom(object source)
{
if (source is Response response)
{
UpdateFrom(response);
}
}

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

writer.WriteString("type", Type);
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 Response 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 Response
{
BodySize = bodySize,
Cookies = cookies,
InternalHeaders = headers?.WhereNotNullValue().ToDictionary(),
StatusCode = statusCode
};
}
}
16 changes: 12 additions & 4 deletions src/Sentry/Request.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ namespace Sentry;
/// <see href="https://develop.sentry.dev/sdk/event-payloads/request/"/>
public sealed class Request : IJsonSerializable
{
internal Dictionary<string, string>? InternalEnv { get; set; }
internal Dictionary<string, string>? InternalEnv { get; private set; }

internal Dictionary<string, string>? InternalOther { get; set; }
internal Dictionary<string, string>? InternalOther { get; private set; }

internal Dictionary<string, string>? InternalHeaders { get; set; }
internal Dictionary<string, string>? InternalHeaders { get; private set; }

/// <summary>
/// Gets or sets the full request URL, if available.
Expand Down Expand Up @@ -91,6 +91,14 @@ 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 Expand Up @@ -158,7 +166,7 @@ public static Request FromJson(JsonElement json)

return new Request
{
InternalEnv = env?.WhereNotNullValue()?.ToDictionary(),
InternalEnv = env?.WhereNotNullValue().ToDictionary(),
InternalOther = other?.WhereNotNullValue().ToDictionary(),
InternalHeaders = headers?.WhereNotNullValue().ToDictionary(),
Url = url,
Expand Down
Loading