Skip to content

Commit

Permalink
feat: Add Client Reports
Browse files Browse the repository at this point in the history
Adds recording discarded events and sending them with client reports to
Sentry. Discarded events give insight into where the SDK drops certain
events like, for example, beforeSend or rate limiting.

Fixes GH-1655
  • Loading branch information
philipphofmann committed Mar 31, 2022
1 parent 2e5882c commit df77385
Show file tree
Hide file tree
Showing 37 changed files with 687 additions and 29 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ This is fixed now by ignoring the sampleRate for transactions. If you use custom

### Various fixes & improvements

- feat: Add Client Reports (#1733)
- fix: Wrongly sampling transactions (#1716)
- feat: Add flag for UIViewControllerTracking (#1711)
- feat: Add more info to touch event breadcrumbs (#1724)
Expand Down
44 changes: 44 additions & 0 deletions Sentry.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Sources/Sentry/Public/SentryEnvelope.h
Expand Up @@ -3,7 +3,7 @@
#import "SentryDefines.h"

@class SentryEvent, SentrySession, SentrySdkInfo, SentryId, SentryUserFeedback, SentryAttachment,
SentryTransaction, SentryTraceState;
SentryTransaction, SentryTraceState, SentryClientReport;

NS_ASSUME_NONNULL_BEGIN

Expand Down
1 change: 1 addition & 0 deletions Sources/Sentry/Public/SentryEnvelopeItemType.h
Expand Up @@ -3,3 +3,4 @@ static NSString *const SentryEnvelopeItemTypeSession = @"session";
static NSString *const SentryEnvelopeItemTypeUserFeedback = @"user_report";
static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction";
static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment";
static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report";
24 changes: 23 additions & 1 deletion Sources/Sentry/SentryClient.m
Expand Up @@ -385,6 +385,11 @@ - (void)storeEnvelope:(SentryEnvelope *)envelope
[self.fileManager storeEnvelope:envelope];
}

- (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason
{
[self.transport recordLostEvent:category reason:reason];
}

- (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
withScope:(SentryScope *)scope
alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace
Expand Down Expand Up @@ -413,6 +418,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) {
[SentryLog logWithMessage:@"Event got sampled, will not send the event"
andLevel:kSentryLevelDebug];
[self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate];
return nil;
}

Expand Down Expand Up @@ -486,9 +492,16 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
}

event = [self callEventProcessors:event];
if (event == nil) {
[self recordLost:eventIsNotATransaction reason:kSentryDiscardReasonEventProcessor];
}

if (nil != self.options.beforeSend) {
if (event != nil && nil != self.options.beforeSend) {
event = self.options.beforeSend(event);

if (event == nil) {
[self recordLost:eventIsNotATransaction reason:kSentryDiscardReasonBeforeSend];
}
}

if (isCrashEvent && nil != self.options.onCrashedLastRun && !SentrySDK.crashedLastRunCalled) {
Expand Down Expand Up @@ -621,6 +634,15 @@ - (void)removeFreeMemoryFromDeviceContext:(SentryEvent *)event
event.context = context;
}

- (void)recordLost:(BOOL)eventIsNotATransaction reason:(SentryDiscardReason)reason
{
if (eventIsNotATransaction) {
[self recordLostEvent:kSentryDataCategoryError reason:reason];
} else {
[self recordLostEvent:kSentryDataCategoryTransaction reason:reason];
}
}

@end

NS_ASSUME_NONNULL_END
33 changes: 33 additions & 0 deletions Sources/Sentry/SentryClientReport.m
@@ -0,0 +1,33 @@
#import "SentryClientReport.h"
#import "SentryCurrentDate.h"
#import <Foundation/Foundation.h>
#import <SentryDiscardedEvent.h>

NS_ASSUME_NONNULL_BEGIN

@implementation SentryClientReport

- (instancetype)initWithDiscardedEvents:(NSArray<SentryDiscardedEvent *> *)discardedEvents
{
if (self = [super init]) {
_timestamp = [SentryCurrentDate date];
_discardedEvents = discardedEvents;
}
return self;
}

- (NSDictionary<NSString *, id> *)serialize
{
NSMutableArray<NSDictionary<NSString *, id> *> *events =
[[NSMutableArray alloc] initWithCapacity:self.discardedEvents.count];
for (SentryDiscardedEvent *event in self.discardedEvents) {
[events addObject:[event serialize]];
}

return
@{ @"timestamp" : @(self.timestamp.timeIntervalSince1970), @"discarded_events" : events };
}

@end

NS_ASSUME_NONNULL_END
13 changes: 13 additions & 0 deletions Sources/Sentry/SentryDataCategoryMapper.m
Expand Up @@ -66,6 +66,19 @@ + (SentryDataCategory)mapIntegerToCategory:(NSUInteger)value
return category;
}

+ (SentryDataCategory)mapStringToCategory:(NSString *)value
{
SentryDataCategory category = kSentryDataCategoryUnknown;

for (int i = 0; i <= kSentryDataCategoryUnknown; i++) {
if ([value isEqualToString:SentryDataCategoryNames[i]]) {
category = [SentryDataCategoryMapper mapIntegerToCategory:i];
}
}

return category;
}

@end

NS_ASSUME_NONNULL_END
34 changes: 34 additions & 0 deletions Sources/Sentry/SentryDiscardReasonMapper.m
@@ -0,0 +1,34 @@
#import "SentryDiscardReasonMapper.h"
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@implementation SentryDiscardReasonMapper

+ (SentryDiscardReason)mapStringToReason:(NSString *)value
{
SentryDiscardReason reason = kSentryDiscardReasonBeforeSend;

for (int i = 0; i <= kSentryDiscardReasonRateLimitBackoff; i++) {
if ([value isEqualToString:SentryDiscardReasonNames[i]]) {
reason = [SentryDiscardReasonMapper mapIntegerToReason:i];
}
}

return reason;
}

+ (SentryDiscardReason)mapIntegerToReason:(NSUInteger)value
{
SentryDiscardReason reason = kSentryDiscardReasonBeforeSend;

if (value <= kSentryDiscardReasonRateLimitBackoff) {
reason = (SentryDiscardReason)value;
}

return reason;
}

@end

NS_ASSUME_NONNULL_END
31 changes: 31 additions & 0 deletions Sources/Sentry/SentryDiscardedEvent.m
@@ -0,0 +1,31 @@
#import "SentryDiscardedEvent.h"
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@implementation SentryDiscardedEvent

- (instancetype)initWithReason:(SentryDiscardReason)reason
category:(SentryDataCategory)category
quantity:(NSUInteger)quantity
{
if (self = [super init]) {
_reason = reason;
_category = category;
_quantity = quantity;
}
return self;
}

- (NSDictionary<NSString *, id> *)serialize
{
return @{
@"reason" : SentryDiscardReasonNames[self.reason],
@"category" : SentryDataCategoryNames[self.category],
@"quantity" : @(self.quantity)
};
}

@end

NS_ASSUME_NONNULL_END
19 changes: 19 additions & 0 deletions Sources/Sentry/SentryEnvelope.m
@@ -1,6 +1,7 @@
#import "SentryEnvelope.h"
#import "SentryAttachment.h"
#import "SentryBreadcrumb.h"
#import "SentryClientReport.h"
#import "SentryEnvelopeItemType.h"
#import "SentryEvent.h"
#import "SentryLog.h"
Expand Down Expand Up @@ -186,6 +187,24 @@ - (instancetype)initWithUserFeedback:(SentryUserFeedback *)userFeedback
data:json];
}

- (instancetype)initWithClientReport:(SentryClientReport *)clientReport
{
NSError *error = nil;
NSData *json = [NSJSONSerialization dataWithJSONObject:[clientReport serialize]
options:0
error:&error];

if (nil != error) {
[SentryLog logWithMessage:@"Couldn't serialize client report." andLevel:kSentryLevelError];
json = [NSData new];
}

return [self initWithHeader:[[SentryEnvelopeItemHeader alloc]
initWithType:SentryEnvelopeItemTypeClientReport
length:json.length]
data:json];
}

- (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment
maxAttachmentSize:(NSUInteger)maxAttachmentSize
{
Expand Down
7 changes: 7 additions & 0 deletions Sources/Sentry/SentryEnvelopeRateLimit.m
Expand Up @@ -10,6 +10,7 @@
SentryEnvelopeRateLimit ()

@property (nonatomic, strong) id<SentryRateLimits> rateLimits;
@property (nonatomic, weak) id<SentryEnvelopeRateLimitDelegate> delegate;

@end

Expand All @@ -23,6 +24,11 @@ - (instancetype)initWithRateLimits:(id<SentryRateLimits>)sentryRateLimits
return self;
}

- (void)setDelegate:(id<SentryEnvelopeRateLimitDelegate>)delegate
{
_delegate = delegate;
}

- (SentryEnvelope *)removeRateLimitedItems:(SentryEnvelope *)envelope
{
if (nil == envelope) {
Expand Down Expand Up @@ -52,6 +58,7 @@ - (SentryEnvelope *)removeRateLimitedItems:(SentryEnvelope *)envelope
[SentryDataCategoryMapper mapEnvelopeItemTypeToCategory:item.header.type];
if ([self.rateLimits isRateLimitActive:rateLimitCategory]) {
[itemsToDrop addObject:item];
[self.delegate envelopeItemDropped:rateLimitCategory];
}
}

Expand Down
8 changes: 8 additions & 0 deletions Sources/Sentry/SentryGlobalEventProcessor.m
Expand Up @@ -24,4 +24,12 @@ - (void)addEventProcessor:(SentryEventProcessor)newProcessor
[self.processors addObject:newProcessor];
}

/**
* Only for testing
*/
- (void)removeAllProcessors
{
[self.processors removeAllObjects];
}

@end
73 changes: 72 additions & 1 deletion Sources/Sentry/SentryHttpTransport.m
@@ -1,6 +1,11 @@
#import "SentryHttpTransport.h"
#import "SentryClientReport.h"
#import "SentryDataCategoryMapper.h"
#import "SentryDiscardReasonMapper.h"
#import "SentryDiscardedEvent.h"
#import "SentryDispatchQueueWrapper.h"
#import "SentryDsn.h"
#import "SentryEnvelope+Private.h"
#import "SentryEnvelope.h"
#import "SentryEnvelopeItemType.h"
#import "SentryEnvelopeRateLimit.h"
Expand All @@ -23,6 +28,15 @@
@property (nonatomic, strong) SentryEnvelopeRateLimit *envelopeRateLimit;
@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue;

/**
* Relay expects the discarded events split by data category and reason; see
* https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload.
* We could use nested dictionaries, but instead, we use a dictionary with key
* `data-category:reason` and value `SentryDiscardedEvent` because it's easier to read and type.
*/
@property (nonatomic, strong)
NSMutableDictionary<NSString *, SentryDiscardedEvent *> *discardedEvents;

/**
* Synching with a dispatch queue to have concurrent reads and writes as barrier blocks is roughly
* 30% slower than using atomic here.
Expand All @@ -48,6 +62,8 @@ - (id)initWithOptions:(SentryOptions *)options
self.envelopeRateLimit = envelopeRateLimit;
self.dispatchQueue = dispatchQueueWrapper;
_isSending = NO;
self.discardedEvents = [NSMutableDictionary new];
[self.envelopeRateLimit setDelegate:self];

[self sendAllCachedEnvelopes];
}
Expand Down Expand Up @@ -145,17 +161,72 @@ - (void)sendEnvelope:(SentryEnvelope *)envelope
return;
}

SentryEnvelope *envelopeToStore = [self addClientReportTo:envelope];

// With this we accept the a tradeoff. We might loose some envelopes when a hard crash happens,
// because this being done on a background thread, but instead we don't block the calling
// thread, which could be the main thread.
[self.dispatchQueue dispatchAsyncWithBlock:^{
[self.fileManager storeEnvelope:envelope];
[self.fileManager storeEnvelope:envelopeToStore];
[self sendAllCachedEnvelopes];
}];
}

- (void)recordLostEvent:(SentryDataCategory)category reason:(SentryDiscardReason)reason
{
NSString *key = [NSString stringWithFormat:@"%@:%@", SentryDataCategoryNames[category],
SentryDiscardReasonNames[reason]];

@synchronized(self.discardedEvents) {
SentryDiscardedEvent *event = self.discardedEvents[key];
NSUInteger quantity = 1;
if (event != nil) {
quantity = event.quantity + 1;
}

event = [[SentryDiscardedEvent alloc] initWithReason:reason
category:category
quantity:quantity];

self.discardedEvents[key] = event;
}
}

/**
* SentryEnvelopeRateLimitDelegate implementation.
*/
- (void)envelopeItemDropped:(SentryDataCategory)dataCategory
{
[self recordLostEvent:dataCategory reason:kSentryDiscardReasonRateLimitBackoff];
}

#pragma mark private methods

- (SentryEnvelope *)addClientReportTo:(SentryEnvelope *)envelope
{
NSArray<SentryDiscardedEvent *> *events;

@synchronized(self.discardedEvents) {
if (self.discardedEvents.count == 0) {
return envelope;
}

events = [self.discardedEvents allValues];
[self.discardedEvents removeAllObjects];
}

SentryClientReport *clientReport = [[SentryClientReport alloc] initWithDiscardedEvents:events];

SentryEnvelopeItem *clientReportEnvelopeItem =
[[SentryEnvelopeItem alloc] initWithClientReport:clientReport];

NSMutableArray<SentryEnvelopeItem *> *currentItems =
[[NSMutableArray alloc] initWithArray:envelope.items];
[currentItems addObject:clientReportEnvelopeItem];

return [[SentryEnvelope alloc] initWithHeader:envelope.header items:currentItems];
}

- (void)sendAllCachedEnvelopes
{
@synchronized(self) {
Expand Down
6 changes: 5 additions & 1 deletion Sources/Sentry/SentryHub.m
Expand Up @@ -240,8 +240,12 @@ - (SentryId *)captureTransaction:(SentryTransaction *)transaction
withScope:(SentryScope *)scope
additionalEnvelopeItems:(NSArray<SentryEnvelopeItem *> *)additionalEnvelopeItems
{
if (transaction.trace.context.sampled != kSentrySampleDecisionYes)
if (transaction.trace.context.sampled != kSentrySampleDecisionYes) {
[self.client recordLostEvent:kSentryDataCategoryTransaction
reason:kSentryDiscardReasonSampleRate];
return SentryId.empty;
}

return [self captureEvent:transaction
withScope:scope
additionalEnvelopeItems:additionalEnvelopeItems];
Expand Down

0 comments on commit df77385

Please sign in to comment.