Skip to content

Commit 8820c5c

Browse files
authored
Add profile rate limiting (#2782)
* added profile rate limiting
1 parent 3bb708a commit 8820c5c

File tree

7 files changed

+66
-8
lines changed

7 files changed

+66
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

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

910
```kotlin

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ public final class io/sentry/DataCategory : java/lang/Enum {
163163
public static final field Attachment Lio/sentry/DataCategory;
164164
public static final field Default Lio/sentry/DataCategory;
165165
public static final field Error Lio/sentry/DataCategory;
166+
public static final field Profile Lio/sentry/DataCategory;
166167
public static final field Security Lio/sentry/DataCategory;
167168
public static final field Session Lio/sentry/DataCategory;
168169
public static final field Transaction Lio/sentry/DataCategory;

sentry/src/main/java/io/sentry/DataCategory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum DataCategory {
1010
Error("error"),
1111
Session("session"),
1212
Attachment("attachment"),
13+
Profile("profile"),
1314
Transaction("transaction"),
1415
Security("security"),
1516
UserReport("user_report"),

sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) {
143143
if (SentryItemType.UserFeedback.equals(itemType)) {
144144
return DataCategory.UserReport;
145145
}
146+
if (SentryItemType.Profile.equals(itemType)) {
147+
return DataCategory.Profile;
148+
}
146149
if (SentryItemType.Attachment.equals(itemType)) {
147150
return DataCategory.Attachment;
148151
}

sentry/src/main/java/io/sentry/transport/RateLimiter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ private boolean isRetryAfter(final @NotNull String itemType) {
145145
return DataCategory.Session;
146146
case "attachment":
147147
return DataCategory.Attachment;
148+
case "profile":
149+
return DataCategory.Profile;
148150
case "transaction":
149151
return DataCategory.Transaction;
150152
default:

sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import io.sentry.DataCategory
55
import io.sentry.DateUtils
66
import io.sentry.EventProcessor
77
import io.sentry.Hint
8+
import io.sentry.IHub
89
import io.sentry.NoOpLogger
10+
import io.sentry.ProfilingTraceData
911
import io.sentry.Sentry
1012
import io.sentry.SentryEnvelope
1113
import io.sentry.SentryEnvelopeHeader
1214
import io.sentry.SentryEnvelopeItem
1315
import io.sentry.SentryEvent
1416
import io.sentry.SentryOptions
17+
import io.sentry.SentryTracer
1518
import io.sentry.Session
19+
import io.sentry.TransactionContext
1620
import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint
1721
import io.sentry.UserFeedback
1822
import io.sentry.dsnString
@@ -21,6 +25,9 @@ import io.sentry.protocol.SentryId
2125
import io.sentry.protocol.SentryTransaction
2226
import io.sentry.protocol.User
2327
import io.sentry.util.HintUtils
28+
import org.mockito.kotlin.mock
29+
import org.mockito.kotlin.whenever
30+
import java.io.File
2431
import java.time.LocalDateTime
2532
import java.time.ZoneId
2633
import java.time.temporal.ChronoUnit
@@ -38,6 +45,9 @@ class ClientReportTest {
3845
@Test
3946
fun `lost envelope can be recorded`() {
4047
givenClientReportRecorder()
48+
val hub = mock<IHub>()
49+
whenever(hub.options).thenReturn(opts)
50+
val transaction = SentryTracer(TransactionContext("name", "op"), hub)
4151

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

5970
clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope)
6071

6172
val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport()
62-
testHelper.assertTotalCount(10, clientReportAtEnd)
73+
testHelper.assertTotalCount(11, clientReportAtEnd)
6374
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd)
6475
testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd)
6576
testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd)
6677
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Error, 1, clientReportAtEnd)
6778
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.UserReport, 1, clientReportAtEnd)
6879
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Session, 1, clientReportAtEnd)
6980
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Attachment, 1, clientReportAtEnd)
81+
testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd)
7082
}
7183

7284
@Test
@@ -89,7 +101,8 @@ class ClientReportTest {
89101
listOf(
90102
DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Error.category, 3),
91103
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Error.category, 2),
92-
DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.Transaction.category, 1)
104+
DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.Transaction.category, 1),
105+
DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Profile.category, 2)
93106
)
94107
)
95108

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

100113
val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport()
101-
testHelper.assertTotalCount(6, clientReportAtEnd)
114+
testHelper.assertTotalCount(8, clientReportAtEnd)
102115
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd)
103116
testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd)
104117
testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd)
118+
testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Profile, 2, clientReportAtEnd)
105119
}
106120

107121
@Test
@@ -112,16 +126,18 @@ class ClientReportTest {
112126
clientReportRecorder.recordLostEvent(DiscardReason.CACHE_OVERFLOW, DataCategory.Attachment)
113127
clientReportRecorder.recordLostEvent(DiscardReason.RATELIMIT_BACKOFF, DataCategory.Error)
114128
clientReportRecorder.recordLostEvent(DiscardReason.QUEUE_OVERFLOW, DataCategory.Error)
129+
clientReportRecorder.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Profile)
115130

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

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

120135
val envelopeReport = envelope.items.first().getClientReport(opts.serializer)!!
121-
assertEquals(3, envelopeReport.discardedEvents.size)
136+
assertEquals(4, envelopeReport.discardedEvents.size)
122137
assertEquals(2, envelopeReport.discardedEvents.first { it.reason == DiscardReason.CACHE_OVERFLOW.reason && it.category == DataCategory.Attachment.category }.quantity)
123138
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.RATELIMIT_BACKOFF.reason && it.category == DataCategory.Error.category }.quantity)
124139
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.QUEUE_OVERFLOW.reason && it.category == DataCategory.Error.category }.quantity)
140+
assertEquals(1, envelopeReport.discardedEvents.first { it.reason == DiscardReason.BEFORE_SEND.reason && it.category == DataCategory.Profile.category }.quantity)
125141
assertTrue(
126142
ChronoUnit.MILLIS.between(
127143
LocalDateTime.now(),

sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.sentry.Hint
55
import io.sentry.IHub
66
import io.sentry.ISerializer
77
import io.sentry.NoOpLogger
8+
import io.sentry.ProfilingTraceData
89
import io.sentry.SentryEnvelope
910
import io.sentry.SentryEnvelopeHeader
1011
import io.sentry.SentryEnvelopeItem
@@ -27,6 +28,7 @@ import org.mockito.kotlin.times
2728
import org.mockito.kotlin.verify
2829
import org.mockito.kotlin.verifyNoMoreInteractions
2930
import org.mockito.kotlin.whenever
31+
import java.io.File
3032
import java.util.UUID
3133
import kotlin.test.Test
3234
import kotlin.test.assertEquals
@@ -185,10 +187,15 @@ class RateLimiterTest {
185187
it.setName("John Me")
186188
}
187189
)
190+
val hub = mock<IHub>()
191+
whenever(hub.options).thenReturn(SentryOptions())
192+
val transaction = SentryTracer(TransactionContext("name", "op"), hub)
193+
188194
val sessionItem = SentryEnvelopeItem.fromSession(fixture.serializer, Session("123", User(), "env", "release"))
189195
val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000)
196+
val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer)
190197

191-
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem))
198+
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem))
192199

193200
rateLimiter.updateRetryAfterLimits(null, null, 429)
194201
val result = rateLimiter.filter(envelope, Hint())
@@ -199,12 +206,15 @@ class RateLimiterTest {
199206
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(sessionItem))
200207
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(userFeedbackItem))
201208
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem))
209+
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem))
202210
verifyNoMoreInteractions(fixture.clientReportRecorder)
203211
}
204212

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

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

223-
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem))
235+
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem))
224236

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

228240
assertNotNull(result)
229-
assertEquals(3, result.items.toList().size)
241+
assertEquals(4, result.items.toList().size)
230242

231243
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(eventItem))
232244
verifyNoMoreInteractions(fixture.clientReportRecorder)
233245
}
246+
247+
@Test
248+
fun `drop profile items as lost`() {
249+
val rateLimiter = fixture.getSUT()
250+
val hub = mock<IHub>()
251+
whenever(hub.options).thenReturn(SentryOptions())
252+
253+
val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent())
254+
val f = File.createTempFile("test", "trace")
255+
val transaction = SentryTracer(TransactionContext("name", "op"), hub)
256+
val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(f, transaction), 1000, fixture.serializer)
257+
val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, profileItem))
258+
259+
rateLimiter.updateRetryAfterLimits("60:profile:key, 1:profile:organization", null, 1)
260+
val result = rateLimiter.filter(envelope, Hint())
261+
262+
assertNotNull(result)
263+
assertEquals(1, result.items.toList().size)
264+
265+
verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem))
266+
verifyNoMoreInteractions(fixture.clientReportRecorder)
267+
}
234268
}

0 commit comments

Comments
 (0)