Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
33 changes: 31 additions & 2 deletions src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand Down
83 changes: 83 additions & 0 deletions test/Sentry.Tests/Internals/Http/HttpTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MockableHttpMessageHandler>();

httpHandler.VerifiableSendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.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<MockableHttpMessageHandler>();

httpHandler.VerifiableSendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.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"));
}
}
Loading