Skip to content

Commit c0f08e7

Browse files
feat(metrics): Add rate limits (#3838)
This adds the handling of rate limits for the new metric_bucket category including handling the metric namespace. Fixes GH-3805
1 parent 6bc7c9c commit c0f08e7

File tree

8 files changed

+88
-30
lines changed

8 files changed

+88
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Add timing API for Metrics (#3812):
8+
- Add [rate limiting](https://develop.sentry.dev/sdk/rate-limiting/) for Metrics (#3838)
89

910
## 8.23.0
1011

Sources/Sentry/SentryDataCategoryMapper.m

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
NSString *const kSentryDataCategoryNameAttachment = @"attachment";
1212
NSString *const kSentryDataCategoryNameUserFeedback = @"user_report";
1313
NSString *const kSentryDataCategoryNameProfile = @"profile";
14-
NSString *const kSentryDataCategoryNameStatsd = @"statsd";
14+
NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket";
1515
NSString *const kSentryDataCategoryNameUnknown = @"unknown";
1616

1717
NS_ASSUME_NONNULL_BEGIN
@@ -34,8 +34,10 @@
3434
if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) {
3535
return kSentryDataCategoryProfile;
3636
}
37+
// The envelope item type used for metrics is statsd whereas the client report category for
38+
// discarded events is metric_bucket.
3739
if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) {
38-
return kSentryDataCategoryStatsd;
40+
return kSentryDataCategoryMetricBucket;
3941
}
4042
return kSentryDataCategoryDefault;
4143
}
@@ -77,8 +79,8 @@
7779
if ([value isEqualToString:kSentryDataCategoryNameProfile]) {
7880
return kSentryDataCategoryProfile;
7981
}
80-
if ([value isEqualToString:kSentryDataCategoryNameStatsd]) {
81-
return kSentryDataCategoryStatsd;
82+
if ([value isEqualToString:kSentryDataCategoryNameMetricBucket]) {
83+
return kSentryDataCategoryMetricBucket;
8284
}
8385

8486
return kSentryDataCategoryUnknown;
@@ -108,8 +110,8 @@
108110
return kSentryDataCategoryNameUserFeedback;
109111
case kSentryDataCategoryProfile:
110112
return kSentryDataCategoryNameProfile;
111-
case kSentryDataCategoryStatsd:
112-
return kSentryDataCategoryNameStatsd;
113+
case kSentryDataCategoryMetricBucket:
114+
return kSentryDataCategoryNameMetricBucket;
113115
case kSentryDataCategoryUnknown:
114116
return kSentryDataCategoryNameUnknown;
115117
}

Sources/Sentry/SentryRateLimitParser.m

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,24 @@ - (instancetype)initWithCurrentDateProvider:(SentryCurrentDateProvider *)current
4545
}
4646

4747
for (NSNumber *category in [self parseCategories:parameters[1]]) {
48-
rateLimits[category] = [self getLongerRateLimit:rateLimits[category]
49-
andRateLimitInSeconds:rateLimitInSeconds];
48+
SentryDataCategory dataCategory
49+
= sentryDataCategoryForNSUInteger(category.integerValue);
50+
51+
// Namespaces should only be available for MetricBucket
52+
if (dataCategory == kSentryDataCategoryMetricBucket && parameters.count > 4) {
53+
NSString *namespacesAsString = parameters[4];
54+
55+
NSArray<NSString *> *namespaces =
56+
[namespacesAsString componentsSeparatedByString:@";"];
57+
if (namespacesAsString.length == 0 || [namespaces containsObject:@"custom"]) {
58+
rateLimits[category] = [self getLongerRateLimit:rateLimits[category]
59+
andRateLimitInSeconds:rateLimitInSeconds];
60+
}
61+
62+
} else {
63+
rateLimits[category] = [self getLongerRateLimit:rateLimits[category]
64+
andRateLimitInSeconds:rateLimitInSeconds];
65+
}
5066
}
5167
}
5268

Sources/Sentry/include/SentryDataCategory.h

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) {
1414
kSentryDataCategoryAttachment = 5,
1515
kSentryDataCategoryUserFeedback = 6,
1616
kSentryDataCategoryProfile = 7,
17-
kSentryDataCategoryStatsd = 8,
17+
kSentryDataCategoryMetricBucket = 8,
1818
kSentryDataCategoryUnknown = 9
1919
};
20-
21-
static DEPRECATED_MSG_ATTRIBUTE(
22-
"Use one of the functions to convert between literals and enum cases in "
23-
"SentryDataCategoryMapper instead.") NSString *_Nonnull const SentryDataCategoryNames[]
24-
= {
25-
@"", // empty on purpose
26-
@"default",
27-
@"error",
28-
@"session",
29-
@"transaction",
30-
@"attachment",
31-
@"user_report",
32-
@"profile",
33-
@"statsd",
34-
@"unkown",
35-
};

Sources/Sentry/include/SentryDataCategoryMapper.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameTransaction;
1111
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameAttachment;
1212
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUserFeedback;
1313
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile;
14-
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameStatsd;
14+
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameMetricBucket;
1515
FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown;
1616

1717
SentryDataCategory sentryDataCategoryForNSUInteger(NSUInteger value);

Tests/SentryTests/Networking/RateLimits/SentryDefaultRateLimitsTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Nimble
12
@testable import Sentry
23
import SentryTestUtils
34
import XCTest
@@ -168,4 +169,46 @@ class SentryDefaultRateLimitsTests: XCTestCase {
168169
XCTAssertFalse(sut.isRateLimitActive(SentryDataCategory.transaction))
169170
XCTAssertFalse(sut.isRateLimitActive(SentryDataCategory.attachment))
170171
}
172+
173+
func testMetricBucket() {
174+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::custom")
175+
176+
sut.update(response)
177+
expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true
178+
}
179+
180+
func testMetricBucket_NoNamespace() {
181+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket::")
182+
183+
sut.update(response)
184+
expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true
185+
}
186+
187+
func testMetricBucket_EmptyNamespace() {
188+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::")
189+
190+
sut.update(response)
191+
expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true
192+
}
193+
194+
func testMetricBucket_NamespaceExclusivelyThanOtherCustom() {
195+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:organization:quota_exceeded:customs;cust")
196+
197+
sut.update(response)
198+
expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == false
199+
}
200+
201+
func testMetricBucket_EmptyNamespaces() {
202+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::;")
203+
204+
sut.update(response)
205+
expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == false
206+
}
207+
208+
func testIgnoreNamespaceForNonMetricBucket() {
209+
let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:error:::customs;cust")
210+
211+
sut.update(response)
212+
expect(self.sut.isRateLimitActive(SentryDataCategory.error)) == true
213+
}
171214
}

Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import Nimble
33
import XCTest
44

55
class SentryDataCategoryMapperTests: XCTestCase {
6+
67
func testEnvelopeItemType() {
78
expect(sentryDataCategoryForEnvelopItemType("event")) == .error
89
expect(sentryDataCategoryForEnvelopItemType("session")) == .session
910
expect(sentryDataCategoryForEnvelopItemType("transaction")) == .transaction
1011
expect(sentryDataCategoryForEnvelopItemType("attachment")) == .attachment
1112
expect(sentryDataCategoryForEnvelopItemType("profile")) == .profile
12-
expect(sentryDataCategoryForEnvelopItemType("statsd")) == .statsd
13+
expect(sentryDataCategoryForEnvelopItemType("statsd")) == .metricBucket
1314
expect(sentryDataCategoryForEnvelopItemType("unknown item type")) == .default
1415
}
1516

@@ -22,7 +23,7 @@ class SentryDataCategoryMapperTests: XCTestCase {
2223
expect(sentryDataCategoryForNSUInteger(5)) == .attachment
2324
expect(sentryDataCategoryForNSUInteger(6)) == .userFeedback
2425
expect(sentryDataCategoryForNSUInteger(7)) == .profile
25-
expect(sentryDataCategoryForNSUInteger(8)) == .statsd
26+
expect(sentryDataCategoryForNSUInteger(8)) == .metricBucket
2627
expect(sentryDataCategoryForNSUInteger(9)) == .unknown
2728

2829
XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(10), "Failed to map unknown category number to case .unknown")
@@ -37,7 +38,7 @@ class SentryDataCategoryMapperTests: XCTestCase {
3738
expect(sentryDataCategoryForString(kSentryDataCategoryNameAttachment)) == .attachment
3839
expect(sentryDataCategoryForString(kSentryDataCategoryNameUserFeedback)) == .userFeedback
3940
expect(sentryDataCategoryForString(kSentryDataCategoryNameProfile)) == .profile
40-
expect(sentryDataCategoryForString(kSentryDataCategoryNameStatsd)) == .statsd
41+
expect(sentryDataCategoryForString(kSentryDataCategoryNameMetricBucket)) == .metricBucket
4142
expect(sentryDataCategoryForString(kSentryDataCategoryNameUnknown)) == .unknown
4243

4344
XCTAssertEqual(.unknown, sentryDataCategoryForString("gdfagdfsa"), "Failed to map unknown category name to case .unknown")
@@ -52,7 +53,7 @@ class SentryDataCategoryMapperTests: XCTestCase {
5253
expect(nameForSentryDataCategory(.attachment)) == kSentryDataCategoryNameAttachment
5354
expect(nameForSentryDataCategory(.userFeedback)) == kSentryDataCategoryNameUserFeedback
5455
expect(nameForSentryDataCategory(.profile)) == kSentryDataCategoryNameProfile
55-
expect(nameForSentryDataCategory(.statsd)) == kSentryDataCategoryNameStatsd
56+
expect(nameForSentryDataCategory(.metricBucket)) == kSentryDataCategoryNameMetricBucket
5657
expect(nameForSentryDataCategory(.unknown)) == kSentryDataCategoryNameUnknown
5758
}
5859
}

Tests/SentryTests/Networking/SentryHttpTransportTests.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,17 @@ class SentryHttpTransportTests: XCTestCase {
344344
assertRateLimitUpdated(response: response)
345345
assertClientReportStoredInMemory()
346346
}
347+
348+
func testSendEventWithMetricBucketRateLimitResponse() {
349+
fixture.requestManager.nextError = NSError(domain: "something", code: 12)
350+
351+
let response = givenRateLimitResponse(forCategory: SentryEnvelopeItemTypeSession)
352+
353+
sendEvent()
354+
355+
assertRateLimitUpdated(response: response)
356+
assertClientReportStoredInMemory()
357+
}
347358

348359
func testSendEnvelopeWithRetryAfterResponse() {
349360
let response = givenRetryAfterResponse()

0 commit comments

Comments
 (0)