Skip to content

Commit 7513600

Browse files
Create a Sentry event for failed HTTP requests (#2320)
1 parent 9923578 commit 7513600

26 files changed

+1426
-194
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Initial work to support profiling in a future release. ([#2206](https://github.com/getsentry/sentry-dotnet/pull/2206))
8+
- Create a Sentry event for failed HTTP requests ([#2320](https://github.com/getsentry/sentry-dotnet/pull/2320))
89
- Improve `WithScope` and add `WithScopeAsync` ([#2303](https://github.com/getsentry/sentry-dotnet/pull/2303)) ([#2309](https://github.com/getsentry/sentry-dotnet/pull/2309))
910
- Build .NET Standard 2.1 for Unity ([#2328](https://github.com/getsentry/sentry-dotnet/pull/2328))
1011
- Add `RemoveExceptionFilter`, `RemoveEventProcessor` and `RemoveTransactionProcessor` extension methods on `SentryOptions` ([#2331](https://github.com/getsentry/sentry-dotnet/pull/2331))

src/Sentry/Contexts.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public sealed class Contexts : ConcurrentDictionary<string, object>, IJsonSerial
3636
/// </remarks>
3737
public OperatingSystem OperatingSystem => this.GetOrCreate<OperatingSystem>(OperatingSystem.Type);
3838

39+
/// <summary>
40+
/// Response interface that contains information on any HTTP response related to the event.
41+
/// </summary>
42+
public Response Response => this.GetOrCreate<Response>(Response.Type);
43+
3944
/// <summary>
4045
/// This describes a runtime in more detail.
4146
/// </summary>
@@ -144,6 +149,10 @@ public static Contexts FromJson(JsonElement json)
144149
{
145150
result[name] = OperatingSystem.FromJson(value);
146151
}
152+
else if (string.Equals(type, Response.Type, StringComparison.OrdinalIgnoreCase))
153+
{
154+
result[name] = Response.FromJson(value);
155+
}
147156
else if (string.Equals(type, Runtime.Type, StringComparison.OrdinalIgnoreCase))
148157
{
149158
result[name] = Runtime.FromJson(value);

src/Sentry/Http/HttpTransportBase.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,6 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env
405405
.SerializeToStringAsync(_options.DiagnosticLogger, _clock, cancellationToken).ConfigureAwait(false);
406406
_options.LogDebug("Failed envelope '{0}' has payload:\n{1}\n", eventId, payload);
407407

408-
409408
// SDK is in debug mode, and envelope was too large. To help troubleshoot:
410409
const string persistLargeEnvelopePathEnvVar = "SENTRY_KEEP_LARGE_ENVELOPE_PATH";
411410
if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Sentry;
2+
3+
internal static class HttpHeadersExtensions
4+
{
5+
internal static string GetCookies(this HttpHeaders headers) =>
6+
headers.TryGetValues("Cookie", out var values)
7+
? string.Join("; ", values)
8+
: string.Empty;
9+
}

src/Sentry/HttpStatusCodeRange.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
namespace Sentry;
2+
3+
/// <summary>
4+
/// Holds a fully-inclusive range of HTTP status codes.
5+
/// e.g. Start = 500, End = 599 represents the range 500-599.
6+
/// </summary>
7+
public readonly record struct HttpStatusCodeRange
8+
{
9+
/// <summary>
10+
/// The inclusive start of the range.
11+
/// </summary>
12+
public int Start { get; init; }
13+
14+
/// <summary>
15+
/// The inclusive end of the range.
16+
/// </summary>
17+
public int End { get; init; }
18+
19+
/// <summary>
20+
/// Creates a range that will only match a single value.
21+
/// </summary>
22+
/// <param name="statusCode">The value in the range.</param>
23+
public HttpStatusCodeRange(int statusCode)
24+
{
25+
Start = statusCode;
26+
End = statusCode;
27+
}
28+
29+
/// <summary>
30+
/// Creates a range that will match all values between <paramref name="start"/> and <paramref name="end"/>.
31+
/// </summary>
32+
/// <param name="start">The inclusive start of the range.</param>
33+
/// <param name="end">The inclusive end of the range.</param>
34+
/// <exception cref="ArgumentOutOfRangeException">
35+
/// Thrown if <paramref name="start"/> is greater than <paramref name="end"/>.
36+
/// </exception>
37+
public HttpStatusCodeRange(int start, int end)
38+
{
39+
if (start > end)
40+
{
41+
throw new ArgumentOutOfRangeException(nameof(start), "Range start must be after range end");
42+
}
43+
44+
Start = start;
45+
End = end;
46+
}
47+
48+
/// <summary>
49+
/// Implicitly converts a tuple of ints to a <see cref="HttpStatusCodeRange"/>.
50+
/// </summary>
51+
/// <param name="range">A tuple of ints to convert.</param>
52+
public static implicit operator HttpStatusCodeRange((int Start, int End) range) => new(range.Start, range.End);
53+
54+
/// <summary>
55+
/// Implicitly converts an int to a <see cref="HttpStatusCodeRange"/>.
56+
/// </summary>
57+
/// <param name="statusCode">An int to convert.</param>
58+
public static implicit operator HttpStatusCodeRange(int statusCode)
59+
{
60+
return new HttpStatusCodeRange(statusCode);
61+
}
62+
63+
/// <summary>
64+
/// Implicitly converts an <see cref="HttpStatusCode"/> to a <see cref="HttpStatusCodeRange"/>.
65+
/// </summary>
66+
/// <param name="statusCode">A status code to convert.</param>
67+
public static implicit operator HttpStatusCodeRange(HttpStatusCode statusCode)
68+
{
69+
return new HttpStatusCodeRange((int)statusCode);
70+
}
71+
72+
/// <summary>
73+
/// Implicitly converts a tuple of <see cref="HttpStatusCode"/> to a <see cref="HttpStatusCodeRange"/>.
74+
/// </summary>
75+
/// <param name="range">A tuple of status codes to convert.</param>
76+
public static implicit operator HttpStatusCodeRange((HttpStatusCode start, HttpStatusCode end) range)
77+
{
78+
return new HttpStatusCodeRange((int)range.start, (int)range.end);
79+
}
80+
81+
/// <summary>
82+
/// Checks if a given status code is contained in the range.
83+
/// </summary>
84+
/// <param name="statusCode">Status code to check.</param>
85+
/// <returns>True if the range contains the given status code.</returns>
86+
public bool Contains(int statusCode)
87+
=> statusCode >= Start && statusCode <= End;
88+
89+
/// <summary>
90+
/// Checks if a given status code is contained in the range.
91+
/// </summary>
92+
/// <param name="statusCode">Status code to check.</param>
93+
/// <returns>True if the range contains the given status code.</returns>
94+
public bool Contains(HttpStatusCode statusCode) => Contains((int)statusCode);
95+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Sentry;
2+
3+
internal interface ISentryFailedRequestHandler
4+
{
5+
void HandleResponse(HttpResponseMessage response);
6+
}

src/Sentry/Protocol/Response.cs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using Sentry.Extensibility;
2+
using Sentry.Internal;
3+
using Sentry.Internal.Extensions;
4+
5+
namespace Sentry.Protocol;
6+
7+
/// <summary>
8+
/// Sentry Response context interface.
9+
/// </summary>
10+
/// <example>
11+
///{
12+
/// "contexts": {
13+
/// "response": {
14+
/// "cookies": "PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1;",
15+
/// "headers": {
16+
/// "content-type": "text/html"
17+
/// /// ...
18+
/// },
19+
/// "status_code": 500,
20+
/// "body_size": 1000, // in bytes
21+
/// }
22+
/// }
23+
///}
24+
/// </example>
25+
/// <see href="https://develop.sentry.dev/sdk/event-payloads/types/#responsecontext"/>
26+
public sealed class Response : IJsonSerializable, ICloneable<Response>, IUpdatable<Response>
27+
{
28+
/// <summary>
29+
/// Tells Sentry which type of context this is.
30+
/// </summary>
31+
public const string Type = "response";
32+
33+
internal Dictionary<string, string>? InternalHeaders { get; private set; }
34+
35+
/// <summary>
36+
/// Gets or sets the HTTP response body size.
37+
/// </summary>
38+
public long? BodySize { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets (optional) cookie values
42+
/// </summary>
43+
public string? Cookies { get; set; }
44+
45+
/// <summary>
46+
/// Gets or sets the headers.
47+
/// </summary>
48+
/// <remarks>
49+
/// If a header appears multiple times it needs to be merged according to the HTTP standard for header merging.
50+
/// </remarks>
51+
public IDictionary<string, string> Headers => InternalHeaders ??= new Dictionary<string, string>();
52+
53+
/// <summary>
54+
/// Gets or sets the HTTP Status response code
55+
/// </summary>
56+
/// <value>The HTTP method.</value>
57+
public short? StatusCode { get; set; }
58+
59+
internal void AddHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
60+
{
61+
foreach (var header in headers)
62+
{
63+
Headers.Add(
64+
header.Key,
65+
string.Join("; ", header.Value)
66+
);
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Clones this instance.
72+
/// </summary>
73+
public Response Clone()
74+
{
75+
var response = new Response();
76+
77+
response.UpdateFrom(this);
78+
79+
return response;
80+
}
81+
82+
/// <summary>
83+
/// Updates this instance with data from the properties in the <paramref name="source"/>,
84+
/// unless there is already a value in the existing property.
85+
/// </summary>
86+
public void UpdateFrom(Response source)
87+
{
88+
BodySize ??= source.BodySize;
89+
Cookies ??= source.Cookies;
90+
StatusCode ??= source.StatusCode;
91+
source.InternalHeaders?.TryCopyTo(Headers);
92+
}
93+
94+
/// <summary>
95+
/// Updates this instance with data from the properties in the <paramref name="source"/>,
96+
/// unless there is already a value in the existing property.
97+
/// </summary>
98+
public void UpdateFrom(object source)
99+
{
100+
if (source is Response response)
101+
{
102+
UpdateFrom(response);
103+
}
104+
}
105+
106+
/// <inheritdoc />
107+
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
108+
{
109+
writer.WriteStartObject();
110+
111+
writer.WriteString("type", Type);
112+
writer.WriteNumberIfNotNull("body_size", BodySize);
113+
writer.WriteStringIfNotWhiteSpace("cookies", Cookies);
114+
writer.WriteStringDictionaryIfNotEmpty("headers", InternalHeaders!);
115+
writer.WriteNumberIfNotNull("status_code", StatusCode);
116+
117+
writer.WriteEndObject();
118+
}
119+
120+
/// <summary>
121+
/// Parses from JSON.
122+
/// </summary>
123+
public static Response FromJson(JsonElement json)
124+
{
125+
var bodySize = json.GetPropertyOrNull("body_size")?.GetInt64();
126+
var cookies = json.GetPropertyOrNull("cookies")?.GetString();
127+
var headers = json.GetPropertyOrNull("headers")?.GetStringDictionaryOrNull();
128+
var statusCode = json.GetPropertyOrNull("status_code")?.GetInt16();
129+
130+
return new Response
131+
{
132+
BodySize = bodySize,
133+
Cookies = cookies,
134+
InternalHeaders = headers?.WhereNotNullValue().ToDictionary(),
135+
StatusCode = statusCode
136+
};
137+
}
138+
}

src/Sentry/Request.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ namespace Sentry;
2626
/// <see href="https://develop.sentry.dev/sdk/event-payloads/request/"/>
2727
public sealed class Request : IJsonSerializable
2828
{
29-
internal Dictionary<string, string>? InternalEnv { get; set; }
29+
internal Dictionary<string, string>? InternalEnv { get; private set; }
3030

31-
internal Dictionary<string, string>? InternalOther { get; set; }
31+
internal Dictionary<string, string>? InternalOther { get; private set; }
3232

33-
internal Dictionary<string, string>? InternalHeaders { get; set; }
33+
internal Dictionary<string, string>? InternalHeaders { get; private set; }
3434

3535
/// <summary>
3636
/// Gets or sets the full request URL, if available.
@@ -91,6 +91,14 @@ public sealed class Request : IJsonSerializable
9191
/// <value>The other.</value>
9292
public IDictionary<string, string> Other => InternalOther ??= new Dictionary<string, string>();
9393

94+
internal void AddHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers)
95+
{
96+
foreach (var header in headers)
97+
{
98+
Headers.Add(header.Key, string.Join("; ", header.Value));
99+
}
100+
}
101+
94102
/// <summary>
95103
/// Clones this instance.
96104
/// </summary>
@@ -158,7 +166,7 @@ public static Request FromJson(JsonElement json)
158166

159167
return new Request
160168
{
161-
InternalEnv = env?.WhereNotNullValue()?.ToDictionary(),
169+
InternalEnv = env?.WhereNotNullValue().ToDictionary(),
162170
InternalOther = other?.WhereNotNullValue().ToDictionary(),
163171
InternalHeaders = headers?.WhereNotNullValue().ToDictionary(),
164172
Url = url,

0 commit comments

Comments
 (0)