diff --git a/src/Sentry/Internal/Http/HttpTransport.cs b/src/Sentry/Internal/Http/HttpTransport.cs index b81c1fccd5..175f6109e7 100644 --- a/src/Sentry/Internal/Http/HttpTransport.cs +++ b/src/Sentry/Internal/Http/HttpTransport.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Sentry.Extensibility; @@ -14,7 +15,10 @@ namespace Sentry.Internal.Http { - internal class HttpTransport : ITransport + /// + /// Internal HTTP Transport logic implementation. This is only meant to be used by this and other Sentry SDKs. + /// + public class HttpTransport : ITransport { private readonly SentryOptions _options; private readonly HttpClient _httpClient; @@ -33,6 +37,9 @@ internal class HttpTransport : ITransport internal const string DefaultErrorMessage = "No message"; + /// + /// Creates the internal HTTP Transport with the given option and HttpClient implementation. + /// public HttpTransport(SentryOptions options, HttpClient httpClient) : this(options, httpClient, Environment.GetEnvironmentVariable) { @@ -46,9 +53,11 @@ internal HttpTransport(SentryOptions options, HttpClient httpClient, _getEnvironmentVariable = getEnvironmentVariable; } - private Envelope ProcessEnvelope(Envelope envelope, DateTimeOffset instant) + /// + /// Re-package the envelope, discarding items that don't fit the rate limit + /// + protected Envelope ProcessEnvelope(Envelope envelope, DateTimeOffset instant) { - // Re-package envelope, discarding items that don't fit the rate limit var envelopeItems = new List(); foreach (var envelopeItem in envelope.Items) { @@ -109,10 +118,20 @@ private Envelope ProcessEnvelope(Envelope envelope, DateTimeOffset instant) } } + if (envelopeItems.Count == 0) + { + _options.LogInfo( + "Envelope {0} was discarded because all contained items are rate-limited.", + envelope.TryGetEventId()); + } + return new Envelope(envelope.Header, envelopeItems); } - private void ExtractRateLimits(HttpResponseMessage response, DateTimeOffset instant) + /// + /// Update local rate limits based on the response from the server. + /// + protected void ExtractRateLimits(HttpResponseMessage response, DateTimeOffset instant) { if (!response.Headers.TryGetValues("X-Sentry-Rate-Limits", out var rateLimitHeaderValues)) { @@ -135,6 +154,7 @@ private void ExtractRateLimits(HttpResponseMessage response, DateTimeOffset inst } } + /// public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default) { var instant = DateTimeOffset.Now; @@ -143,10 +163,6 @@ public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancell using var processedEnvelope = ProcessEnvelope(envelope, instant); if (processedEnvelope.Items.Count == 0) { - _options.LogInfo( - "Envelope {0} was discarded because all contained items are rate-limited.", - envelope.TryGetEventId()); - return; } @@ -160,10 +176,8 @@ public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancell if (response.StatusCode != HttpStatusCode.OK) { await HandleFailureAsync(response, processedEnvelope, cancellationToken).ConfigureAwait(false); - return; } - - if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Debug) is true) + else if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Debug) is true) { _options.LogDebug("Envelope '{0}' sent successfully. Payload:\n{1}", envelope.TryGetEventId(), @@ -171,8 +185,7 @@ public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancell } else { - _options.LogInfo("Envelope '{0}' successfully received by Sentry.", - processedEnvelope.TryGetEventId()); + _options.LogInfo("Envelope '{0}' successfully received by Sentry.", processedEnvelope.TryGetEventId()); } } @@ -182,42 +195,18 @@ private async Task HandleFailureAsync( CancellationToken cancellationToken) { // Spare the overhead if level is not enabled - if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && - response.Content is { } content) + if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && response.Content is { } content) { if (string.Equals(content.Headers.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) { - var responseJson = await content.ReadAsJsonAsync(cancellationToken).ConfigureAwait(false); - - var errorMessage = - responseJson.GetPropertyOrNull("detail")?.GetString() - ?? DefaultErrorMessage; - - var errorCauses = - responseJson.GetPropertyOrNull("causes")?.EnumerateArray().Select(j => j.GetString()).ToArray() - ?? Array.Empty(); - - _options.Log( - SentryLevel.Error, - "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}. Error causes: {3}.", - null, - processedEnvelope.TryGetEventId(), - response.StatusCode, - errorMessage, - string.Join(", ", errorCauses)); + LogFailure(response, processedEnvelope, + await content.ReadAsJsonAsync(cancellationToken).ConfigureAwait(false)); } else { - var responseString = await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - - _options.Log( - SentryLevel.Error, - "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}.", - null, - processedEnvelope.TryGetEventId(), - response.StatusCode, - responseString); + LogFailure(response, processedEnvelope, + await content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); } // If debug level, dump the whole envelope to the logger @@ -260,7 +249,47 @@ private async Task HandleFailureAsync( } } - internal HttpRequestMessage CreateRequest(Envelope envelope) + /// + /// Log failure response. + /// + protected void LogFailure(HttpResponseMessage response, Envelope processedEnvelope, JsonElement responseJson) + { + var errorMessage = + responseJson.GetPropertyOrNull("detail")?.GetString() + ?? DefaultErrorMessage; + + var errorCauses = + responseJson.GetPropertyOrNull("causes")?.EnumerateArray().Select(j => j.GetString()).ToArray() + ?? Array.Empty(); + + _options.Log( + SentryLevel.Error, + "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}. Error causes: {3}.", + null, + processedEnvelope.TryGetEventId(), + response.StatusCode, + errorMessage, + string.Join(", ", errorCauses)); + } + + /// + /// Log failure response. + /// + protected void LogFailure(HttpResponseMessage response, Envelope processedEnvelope, string responseString) + { + _options.Log( + SentryLevel.Error, + "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}.", + null, + processedEnvelope.TryGetEventId(), + response.StatusCode, + responseString); + } + + /// + /// Create HTTP request for the envelope. + /// + protected internal HttpRequestMessage CreateRequest(Envelope envelope) { if (string.IsNullOrWhiteSpace(_options.Dsn)) { diff --git a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index 5c965ff1ab..432cd2efbd 100644 --- a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.Core3_1.verified.txt +++ b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.Core3_1.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt index 52dfab4018..ec88b6bd5e 100644 --- a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt +++ b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 7d15fad0ba..3c903b5290 100644 --- a/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.DiagnosticSource.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core2_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core2_1.verified.txt index 617e44983b..703d67096e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core2_1.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core2_1.verified.txt @@ -1096,6 +1096,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_0.verified.txt index f099611441..5fe8c77085 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_0.verified.txt @@ -1096,6 +1096,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index 5c965ff1ab..432cd2efbd 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt index d729db9f36..52999881d8 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_6.verified.txt @@ -1096,6 +1096,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt index 52dfab4018..ec88b6bd5e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet5_0.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 7d15fad0ba..3c903b5290 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -1097,6 +1097,19 @@ namespace Sentry.Integrations void Register(Sentry.IHub hub, Sentry.SentryOptions options); } } +namespace Sentry.Internal.Http +{ + public class HttpTransport + { + public HttpTransport(Sentry.SentryOptions options, System.Net.Http.HttpClient httpClient) { } + protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected void ExtractRateLimits(System.Net.Http.HttpResponseMessage response, System.DateTimeOffset instant) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, string responseString) { } + protected void LogFailure(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope processedEnvelope, System.Text.Json.JsonElement responseJson) { } + protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope, System.DateTimeOffset instant) { } + public System.Threading.Tasks.Task SendEnvelopeAsync(Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken = default) { } + } +} namespace Sentry.PlatformAbstractions { public static class FrameworkInfo diff --git a/test/Sentry.Tests/Internals/AccessModifierTests.cs b/test/Sentry.Tests/Internals/AccessModifierTests.cs index ae6e057344..8c9d170fc4 100644 --- a/test/Sentry.Tests/Internals/AccessModifierTests.cs +++ b/test/Sentry.Tests/Internals/AccessModifierTests.cs @@ -14,7 +14,10 @@ public void TypesInInternalsNamespace_AreNotPublic() Assert.All(types, type => { - Assert.False(type.IsPublic, $"Expected type {type.Name} to be internal."); + if (type.Name != "HttpTransport") + { + Assert.False(type.IsPublic, $"Expected type {type.Name} to be internal."); + } }); } }