diff --git a/CHANGELOG.md b/CHANGELOG.md index e68a66bc71..f699acc9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- The SDK now logs a `Warning` instead of an `Error` when being ratelimited ([#4927](https://github.com/getsentry/sentry-dotnet/pull/4927)) + ## 6.2.0-alpha.0 ### Features diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs index d19bae1961..d9e1ed53f5 100644 --- a/src/Sentry/Http/HttpTransportBase.cs +++ b/src/Sentry/Http/HttpTransportBase.cs @@ -359,7 +359,8 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope) var eventId = envelope.TryGetEventId(_options.DiagnosticLogger); // Spare the overhead if level is not enabled - if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && response.Content is { } content) + var minLogLevel = response.StatusCode == (HttpStatusCode)429 ? SentryLevel.Warning : SentryLevel.Error; + if (_options.DiagnosticLogger?.IsEnabled(minLogLevel) is true && response.Content is { } content) { if (HasJsonContent(content)) { @@ -428,7 +429,8 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env var eventId = envelope.TryGetEventId(_options.DiagnosticLogger); // Spare the overhead if level is not enabled - if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Error) is true && response.Content is { } content) + var minLogLevel = response.StatusCode == (HttpStatusCode)429 ? SentryLevel.Warning : SentryLevel.Error; + if (_options.DiagnosticLogger?.IsEnabled(minLogLevel) is true && response.Content is { } content) { if (HasJsonContent(content)) { @@ -525,6 +527,12 @@ private void LogFailure(string responseString, HttpStatusCode responseStatusCode return; } + if (responseStatusCode == (HttpStatusCode)429) + { + LogRateLimited(eventId, responseString); + return; + } + _options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}.", _typeName, eventId, @@ -551,6 +559,15 @@ private void LogFailure(JsonElement responseJson, HttpStatusCode responseStatusC return; } + if (responseStatusCode == (HttpStatusCode)429) + { + var responseDetail = errorCauses.Length > 0 + ? $"{errorMessage} ({string.Join(", ", errorCauses)})" + : errorMessage; + LogRateLimited(eventId, responseDetail); + return; + } + _options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}. Error causes: {4}.", _typeName, eventId, @@ -570,6 +587,18 @@ private void LogRequestTooLarge(SentryId? eventId, string responseDetail) responseDetail); } + private void LogRateLimited(SentryId? eventId, string responseDetail) + { + _options.LogWarning( + "{0}: Sentry rejected the envelope '{1}' due to rate limiting. " + + "This may indicate that you are sending too much data or have exceeded your quota. " + + "See https://docs.sentry.io/product/accounts/quotas/ for more information. " + + "Server response: {2}", + _typeName, + eventId, + responseDetail); + } + private static bool HasJsonContent(HttpContent content) => string.Equals(content.Headers.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase); diff --git a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs index aa828064c9..179f1f728c 100644 --- a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs @@ -989,4 +989,87 @@ public async Task SendEnvelopeAsync_Response413_RecordsSendErrorDiscard() recorder.DiscardedEvents.Should().ContainKey(DiscardReason.SendError.WithCategory(DataCategory.Error)); recorder.DiscardedEvents.Should().NotContainKey(DiscardReason.NetworkError.WithCategory(DataCategory.Error)); } + + [Fact] + public async Task SendEnvelopeAsync_Response429WithJsonMessage_LogsWarning() + { + // Arrange + const string expectedDetail = "Sentry dropped data due to a quota or internal rate limit being reached."; + + var httpHandler = Substitute.For(); + + httpHandler.VerifiableSendAsync(Arg.Any(), Arg.Any()) + .Returns(_ => SentryResponses.GetJsonErrorResponse((HttpStatusCode)429, expectedDetail)); + + var logger = new InMemoryDiagnosticLogger(); + + var httpTransport = new HttpTransport( + new SentryOptions + { + Dsn = ValidDsn, + Debug = true, + DiagnosticLogger = logger + }, + new HttpClient(httpHandler)); + + var envelope = Envelope.FromEvent(new SentryEvent()); + + // Act + await httpTransport.SendEnvelopeAsync(envelope); + + // Assert + var warningEntry = logger.Entries.FirstOrDefault(e => + e.Level == SentryLevel.Warning && + e.Message.Contains("due to rate limiting")); + + warningEntry.Should().NotBeNull(); + warningEntry!.Message.Should().Contain("exceeded your quota"); + warningEntry.Args[2].ToString().Should().Contain(expectedDetail); + + // Should NOT have an error-level log for this + logger.Entries.Should().NotContain(e => + e.Level == SentryLevel.Error && + e.Message.Contains("Sentry rejected the envelope")); + } + + [Fact] + public async Task SendEnvelopeAsync_Response429WithTextMessage_LogsWarning() + { + // Arrange + const string expectedMessage = "Rate limited"; + + var httpHandler = Substitute.For(); + + httpHandler.VerifiableSendAsync(Arg.Any(), Arg.Any()) + .Returns(_ => SentryResponses.GetTextErrorResponse((HttpStatusCode)429, expectedMessage)); + + var logger = new InMemoryDiagnosticLogger(); + + var httpTransport = new HttpTransport( + new SentryOptions + { + Dsn = ValidDsn, + Debug = true, + DiagnosticLogger = logger + }, + new HttpClient(httpHandler)); + + var envelope = Envelope.FromEvent(new SentryEvent()); + + // Act + await httpTransport.SendEnvelopeAsync(envelope); + + // Assert + var warningEntry = logger.Entries.FirstOrDefault(e => + e.Level == SentryLevel.Warning && + e.Message.Contains("due to rate limiting")); + + warningEntry.Should().NotBeNull(); + warningEntry!.Args[2].ToString().Should().Contain(expectedMessage); + + // Should NOT have an error-level log for this + logger.Entries.Should().NotContain(e => + e.Level == SentryLevel.Error && + e.Message.Contains("Sentry rejected the envelope")); + } }