Skip to content

Commit

Permalink
Add profile rate limiting (#2782)
Browse files Browse the repository at this point in the history
* added profile rate limiting
  • Loading branch information
stefanosiano authored Jun 13, 2023
1 parent 3bb708a commit 8820c5c
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 8 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add profile rate limiting ([#2782](https://github.com/getsentry/sentry-java/pull/2782))
- Support for automatically capturing Failed GraphQL (Apollo 3) Client errors ([#2781](https://github.com/getsentry/sentry-java/pull/2781))

```kotlin
Expand Down
1 change: 1 addition & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ public final class io/sentry/DataCategory : java/lang/Enum {
public static final field Attachment Lio/sentry/DataCategory;
public static final field Default Lio/sentry/DataCategory;
public static final field Error Lio/sentry/DataCategory;
public static final field Profile Lio/sentry/DataCategory;
public static final field Security Lio/sentry/DataCategory;
public static final field Session Lio/sentry/DataCategory;
public static final field Transaction Lio/sentry/DataCategory;
Expand Down
1 change: 1 addition & 0 deletions sentry/src/main/java/io/sentry/DataCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum DataCategory {
Error("error"),
Session("session"),
Attachment("attachment"),
Profile("profile"),
Transaction("transaction"),
Security("security"),
UserReport("user_report"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) {
if (SentryItemType.UserFeedback.equals(itemType)) {
return DataCategory.UserReport;
}
if (SentryItemType.Profile.equals(itemType)) {
return DataCategory.Profile;
}
if (SentryItemType.Attachment.equals(itemType)) {
return DataCategory.Attachment;
}
Expand Down
2 changes: 2 additions & 0 deletions sentry/src/main/java/io/sentry/transport/RateLimiter.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ private boolean isRetryAfter(final @NotNull String itemType) {
return DataCategory.Session;
case "attachment":
return DataCategory.Attachment;
case "profile":
return DataCategory.Profile;
case "transaction":
return DataCategory.Transaction;
default:
Expand Down
26 changes: 21 additions & 5 deletions sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import io.sentry.DataCategory
import io.sentry.DateUtils
import io.sentry.EventProcessor
import io.sentry.Hint
import io.sentry.IHub
import io.sentry.NoOpLogger
import io.sentry.ProfilingTraceData
import io.sentry.Sentry
import io.sentry.SentryEnvelope
import io.sentry.SentryEnvelopeHeader
import io.sentry.SentryEnvelopeItem
import io.sentry.SentryEvent
import io.sentry.SentryOptions
import io.sentry.SentryTracer
import io.sentry.Session
import io.sentry.TransactionContext
import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint
import io.sentry.UserFeedback
import io.sentry.dsnString
Expand All @@ -21,6 +25,9 @@ import io.sentry.protocol.SentryId
import io.sentry.protocol.SentryTransaction
import io.sentry.protocol.User
import io.sentry.util.HintUtils
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import java.io.File
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.temporal.ChronoUnit
Expand All @@ -38,6 +45,9 @@ class ClientReportTest {
@Test
fun `lost envelope can be recorded`() {
givenClientReportRecorder()
val hub = mock<IHub>()
whenever(hub.options).thenReturn(opts)
val transaction = SentryTracer(TransactionContext("name", "op"), hub)

val lostClientReport = ClientReport(
DateUtils.getCurrentDateTime(),
Expand All @@ -53,20 +63,22 @@ class ClientReportTest {
SentryEnvelopeItem.fromEvent(opts.serializer, SentryEvent()),
SentryEnvelopeItem.fromSession(opts.serializer, Session("dis", User(), "env", "0.0.1")),
SentryEnvelopeItem.fromUserFeedback(opts.serializer, UserFeedback(SentryId(UUID.randomUUID()))),
SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000)
SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000),
SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer)
)

clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope)

val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport()
testHelper.assertTotalCount(10, clientReportAtEnd)
testHelper.assertTotalCount(11, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Error, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.UserReport, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Session, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Attachment, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd)
}

@Test
Expand All @@ -89,7 +101,8 @@ class ClientReportTest {
listOf(
DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 3),
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 2),
DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.Transaction.category, 1)
DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.Transaction.category, 1),
DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Profile.category, 2)
)
)

Expand All @@ -98,10 +111,11 @@ class ClientReportTest {
clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelopeItem)

val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport()
testHelper.assertTotalCount(6, clientReportAtEnd)
testHelper.assertTotalCount(8, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd)
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Profile, 2, clientReportAtEnd)
}

@Test
Expand All @@ -112,16 +126,18 @@ class ClientReportTest {
clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment)
clientReportRecorder.recordLostEvent(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error)
clientReportRecorder.recordLostEvent(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error)
clientReportRecorder.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Profile)

val envelope = clientReportRecorder.attachReportToEnvelope(testHelper.newEnvelope())

testHelper.assertTotalCount(0, clientReportRecorder.resetCountsAndGenerateClientReport())

val envelopeReport = envelope.items.first().getClientReport(opts.serializer)!!
assertEquals(3, envelopeReport.discardedEvents.size)
assertEquals(4, envelopeReport.discardedEvents.size)
assertEquals(2, envelopeReport.discardedEvents.first { it.reason == DiscardReason.CACHE_OVERFLOW.reason && it.category == DataCategory.Attachment.category }.quantity)
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.RATELIMIT_BACKOFF.reason && it.category == DataCategory.Error.category }.quantity)
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.QUEUE_OVERFLOW.reason && it.category == DataCategory.Error.category }.quantity)
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.BEFORE_SEND.reason && it.category == DataCategory.Profile.category }.quantity)
assertTrue(
ChronoUnit.MILLIS.between(
LocalDateTime.now(),
Expand Down
40 changes: 37 additions & 3 deletions sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.sentry.Hint
import io.sentry.IHub
import io.sentry.ISerializer
import io.sentry.NoOpLogger
import io.sentry.ProfilingTraceData
import io.sentry.SentryEnvelope
import io.sentry.SentryEnvelopeHeader
import io.sentry.SentryEnvelopeItem
Expand All @@ -27,6 +28,7 @@ import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.io.File
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -185,10 +187,15 @@ class RateLimiterTest {
it.setName("John Me")
}
)
val hub = mock<IHub>()
whenever(hub.options).thenReturn(SentryOptions())
val transaction = SentryTracer(TransactionContext("name", "op"), hub)

val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release"))
val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000)
val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer)

val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem))
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem))

rateLimiter.updateRetryAfterLimits(null, null, 429)
val result = rateLimiter.filter(envelope, Hint())
Expand All @@ -199,12 +206,15 @@ class RateLimiterTest {
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(sessionItem))
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(userFeedbackItem))
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem))
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem))
verifyNoMoreInteractions(fixture.clientReportRecorder)
}

@Test
fun `records only dropped items as lost`() {
val rateLimiter = fixture.getSUT()
val hub = mock<IHub>()
whenever(hub.options).thenReturn(SentryOptions())

val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent())
val userFeedbackItem = SentryEnvelopeItem.fromUserFeedback(
Expand All @@ -217,18 +227,42 @@ class RateLimiterTest {
it.setName("John Me")
}
)
val transaction = SentryTracer(TransactionContext("name", "op"), hub)
val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer)
val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release"))
val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000)

val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem))
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem))

rateLimiter.updateRetryAfterLimits("60:error:key, 1:error:organization", null, 1)
val result = rateLimiter.filter(envelope, Hint())

assertNotNull(result)
assertEquals(3, result.items.toList().size)
assertEquals(4, result.items.toList().size)

verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(eventItem))
verifyNoMoreInteractions(fixture.clientReportRecorder)
}

@Test
fun `drop profile items as lost`() {
val rateLimiter = fixture.getSUT()
val hub = mock<IHub>()
whenever(hub.options).thenReturn(SentryOptions())

val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent())
val f = File.createTempFile("test", "trace")
val transaction = SentryTracer(TransactionContext("name", "op"), hub)
val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(f, transaction), 1000, fixture.serializer)
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, profileItem))

rateLimiter.updateRetryAfterLimits("60:profile:key, 1:profile:organization", null, 1)
val result = rateLimiter.filter(envelope, Hint())

assertNotNull(result)
assertEquals(1, result.items.toList().size)

verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem))
verifyNoMoreInteractions(fixture.clientReportRecorder)
}
}

0 comments on commit 8820c5c

Please sign in to comment.