Skip to content

Commit cdbfb21

Browse files
authored
ref(rate-limit): handle profile and check_in rate limiting (#1970)
1 parent 10bf447 commit cdbfb21

File tree

3 files changed

+129
-5
lines changed

3 files changed

+129
-5
lines changed

src/Transport/HttpTransport.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ public function send(Event $event): Result
100100
return new Result(ResultStatus::rateLimit());
101101
}
102102

103+
// Since profiles are attached to transaction we have to check separately if they are rate limited.
104+
// We can do this after transactions have been checked because if transactions are rate limited,
105+
// so are profiles but not the other way around.
106+
if ($event->getSdkMetadata('profile') !== null) {
107+
if ($this->rateLimiter->isRateLimited(RateLimiter::DATA_CATEGORY_PROFILE)) {
108+
// Just remove profiling data so the normal transaction can be sent.
109+
$event->setSdkMetadata('profile', null);
110+
$this->logger->warning(
111+
'Rate limit exceeded for sending requests of type "profile". The profile has been dropped.',
112+
['event' => $event]
113+
);
114+
}
115+
}
116+
103117
$request = new Request();
104118
$request->setStringBody($this->payloadSerializer->serialize($event));
105119

src/Transport/RateLimiter.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
final class RateLimiter
1313
{
14+
/**
15+
* @var string
16+
*/
17+
public const DATA_CATEGORY_PROFILE = 'profile';
18+
1419
/**
1520
* @var string
1621
*/
@@ -21,6 +26,11 @@ final class RateLimiter
2126
*/
2227
private const DATA_CATEGORY_LOG_ITEM = 'log_item';
2328

29+
/**
30+
* @var string
31+
*/
32+
private const DATA_CATEGORY_CHECK_IN = 'monitor';
33+
2434
/**
2535
* The name of the header to look at to know the rate limits for the events
2636
* categories supported by the server.
@@ -103,9 +113,7 @@ public function handleResponse(Response $response): bool
103113
*/
104114
public function isRateLimited($eventType): bool
105115
{
106-
$disabledUntil = $this->getDisabledUntil($eventType);
107-
108-
return $disabledUntil > time();
116+
return $this->getDisabledUntil($eventType) > time();
109117
}
110118

111119
/**
@@ -119,6 +127,8 @@ public function getDisabledUntil($eventType): int
119127
$eventType = self::DATA_CATEGORY_ERROR;
120128
} elseif ($eventType === 'log') {
121129
$eventType = self::DATA_CATEGORY_LOG_ITEM;
130+
} elseif ($eventType === 'check_in') {
131+
$eventType = self::DATA_CATEGORY_CHECK_IN;
122132
}
123133

124134
return max($this->rateLimits['all'] ?? 0, $this->rateLimits[$eventType] ?? 0);

tests/Transport/HttpTransportTest.php

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Sentry\HttpClient\HttpClientInterface;
1313
use Sentry\HttpClient\Response;
1414
use Sentry\Options;
15+
use Sentry\Profiling\Profile;
1516
use Sentry\Serializer\PayloadSerializerInterface;
1617
use Sentry\Transport\HttpTransport;
1718
use Sentry\Transport\ResultStatus;
@@ -25,7 +26,7 @@ final class HttpTransportTest extends TestCase
2526
private $logger;
2627

2728
/**
28-
* @var MockObject&HttpAsyncClientInterface
29+
* @var MockObject&HttpClientInterface
2930
*/
3031
private $httpClient;
3132

@@ -180,7 +181,7 @@ public function testSendFailsDueToHttpClientException(): void
180181
$this->assertSame(ResultStatus::failed(), $result->getStatus());
181182
}
182183

183-
public function testSendFailsDueToCulrError(): void
184+
public function testSendFailsDueToCurlError(): void
184185
{
185186
$event = Event::createEvent();
186187

@@ -263,6 +264,105 @@ public function testSendFailsDueToExceedingRateLimits(): void
263264
$this->assertSame(ResultStatus::rateLimit(), $result->getStatus());
264265
}
265266

267+
/**
268+
* @group time-sensitive
269+
*/
270+
public function testDropsProfileAndSendsTransactionWhenProfileRateLimited(): void
271+
{
272+
ClockMock::withClockMock(1644105600);
273+
274+
$transport = new HttpTransport(
275+
new Options(['dsn' => 'http://[email protected]/1']),
276+
$this->httpClient,
277+
$this->payloadSerializer,
278+
$this->logger
279+
);
280+
281+
$event = Event::createTransaction();
282+
$event->setSdkMetadata('profile', new Profile());
283+
284+
$this->payloadSerializer->expects($this->exactly(2))
285+
->method('serialize')
286+
->willReturn('{"foo":"bar"}');
287+
288+
$this->httpClient->expects($this->exactly(2))
289+
->method('sendRequest')
290+
->willReturnOnConsecutiveCalls(
291+
new Response(429, ['X-Sentry-Rate-Limits' => ['60:profile:key']], ''),
292+
new Response(200, [], '')
293+
);
294+
295+
// First request is rate limited because of profiles
296+
$result = $transport->send($event);
297+
298+
$this->assertEquals(ResultStatus::rateLimit(), $result->getStatus());
299+
300+
// profile information is still present
301+
$this->assertNotNull($event->getSdkMetadata('profile'));
302+
303+
$event = Event::createTransaction();
304+
$event->setSdkMetadata('profile', new Profile());
305+
306+
$this->logger->expects($this->once())
307+
->method('warning')
308+
->with(
309+
$this->stringContains('Rate limit exceeded for sending requests of type "profile".'),
310+
['event' => $event]
311+
);
312+
313+
$result = $transport->send($event);
314+
315+
// Sending transaction is successful because only profiles are rate limited
316+
$this->assertEquals(ResultStatus::success(), $result->getStatus());
317+
318+
// profile information is removed because it was rate limited
319+
$this->assertNull($event->getSdkMetadata('profile'));
320+
}
321+
322+
/**
323+
* @group time-sensitive
324+
*/
325+
public function testCheckInsAreRateLimited(): void
326+
{
327+
ClockMock::withClockMock(1644105600);
328+
329+
$transport = new HttpTransport(
330+
new Options(['dsn' => 'http://[email protected]/1']),
331+
$this->httpClient,
332+
$this->payloadSerializer,
333+
$this->logger
334+
);
335+
336+
$event = Event::createCheckIn();
337+
338+
$this->payloadSerializer->expects($this->exactly(1))
339+
->method('serialize')
340+
->willReturn('{"foo":"bar"}');
341+
342+
$this->httpClient->expects($this->exactly(1))
343+
->method('sendRequest')
344+
->willReturn(
345+
new Response(429, ['X-Sentry-Rate-Limits' => ['60:monitor:key']], '')
346+
);
347+
348+
$result = $transport->send($event);
349+
350+
$this->assertEquals(ResultStatus::rateLimit(), $result->getStatus());
351+
352+
$event = Event::createCheckIn();
353+
354+
$this->logger->expects($this->once())
355+
->method('warning')
356+
->with(
357+
$this->stringContains('Rate limit exceeded for sending requests of type "check_in".'),
358+
['event' => $event]
359+
);
360+
361+
$result = $transport->send($event);
362+
363+
$this->assertEquals(ResultStatus::rateLimit(), $result->getStatus());
364+
}
365+
266366
public function testClose(): void
267367
{
268368
$transport = new HttpTransport(

0 commit comments

Comments
 (0)