Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLAT-8749] Trim breadcrumbs in oversized payloads #1451

Merged
merged 1 commit into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions Bugsnag/Delivery/BSGEventUploadOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,12 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
return;
}

NSDictionary *_Nullable originalPayload = nil;
NSDictionary *retryPayload = nil;
for (BugsnagOnSendErrorBlock block in configuration.onSendBlocks) {
@try {
if (!originalPayload) {
if (!retryPayload) {
// If OnSendError modifies the event and delivery fails, we need to persist the original state of the event.
originalPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys];
retryPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys];
}
if (!block(event)) {
[self deleteEvent];
Expand All @@ -97,8 +97,11 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
@try {
[event truncateStrings:configuration.maxStringValueLength];
eventPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys];
if (!retryPayload || [retryPayload isEqualToDictionary:eventPayload]) {
retryPayload = eventPayload;
}
} @catch (NSException *exception) {
bsg_log_err(@"Discarding event %@ because an exception was thrown by -toJsonWithRedactedKeys: %@", self.name, exception);
bsg_log_err(@"Discarding event %@ due to exception %@", self.name, exception);
[BSGInternalErrorReporter.sharedInstance reportException:exception diagnostics:nil groupingHash:
[NSString stringWithFormat:@"BSGEventUploadOperation -[runWithDelegate:completionHandler:] %@ %@",
exception.name, exception.reason]];
Expand All @@ -107,11 +110,6 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
return;
}

if ([originalPayload isEqual:eventPayload]) {
// Save memory if payload has not changed
originalPayload = nil;
}

NSString *apiKey = event.apiKey ?: configuration.apiKey;

NSMutableDictionary *requestPayload = [NSMutableDictionary dictionary];
Expand Down Expand Up @@ -140,6 +138,26 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
return;
}

if (data.length > MaxPersistedSize) {
// Trim extra bytes to make space for "removed" message and usage telemetry.
NSUInteger bytesToRemove = data.length - (MaxPersistedSize - 300);
bsg_log_debug(@"Trimming breadcrumbs; bytesToRemove = %lu", (unsigned long)bytesToRemove);
@try {
[event trimBreadcrumbs:bytesToRemove];
eventPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys];
requestPayload[BSGKeyEvents] = @[eventPayload];
data = BSGJSONDataFromDictionary(requestPayload, NULL);
} @catch (NSException *exception) {
bsg_log_err(@"Discarding event %@ due to exception %@", self.name, exception);
[BSGInternalErrorReporter.sharedInstance reportException:exception diagnostics:nil groupingHash:
[NSString stringWithFormat:@"BSGEventUploadOperation -[runWithDelegate:completionHandler:] %@ %@",
exception.name, exception.reason]];
[self deleteEvent];
completionHandler();
return;
}
}

BSGPostJSONData(configuration.session, data, requestHeaders, notifyURL, ^(BSGDeliveryStatus status, __unused NSError *deliveryError) {
switch (status) {
case BSGDeliveryStatusDelivered:
Expand All @@ -149,7 +167,7 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion

case BSGDeliveryStatusFailed:
bsg_log_debug(@"Upload failed retryably for event %@", self.name);
[self prepareForRetry:originalPayload ?: eventPayload HTTPBodySize:data.length];
[self prepareForRetry:retryPayload HTTPBodySize:data.length];
break;

case BSGDeliveryStatusUndeliverable:
Expand Down
2 changes: 2 additions & 0 deletions Bugsnag/Payload/BugsnagEvent+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN

- (NSDictionary *)toJsonWithRedactedKeys:(nullable NSSet *)redactedKeys;

- (void)trimBreadcrumbs:(NSUInteger)bytesToRemove;

- (void)truncateStrings:(NSUInteger)maxLength;

- (void)notifyUnhandledOverridden;
Expand Down
36 changes: 36 additions & 0 deletions Bugsnag/Payload/BugsnagEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#import "BSGDefines.h"
#import "BSGFeatureFlagStore.h"
#import "BSGJSONSerialization.h"
#import "BSGKeys.h"
#import "BSGSerialization.h"
#import "BSGUtils.h"
Expand Down Expand Up @@ -690,6 +691,41 @@ - (void)symbolicateIfNeeded {
}
}

- (void)trimBreadcrumbs:(const NSUInteger)bytesToRemove {
NSMutableArray *breadcrumbs = [self.breadcrumbs mutableCopy];
BugsnagBreadcrumb *lastRemovedBreadcrumb = nil;
NSUInteger bytesRemoved = 0, count = 0;

while (bytesRemoved < bytesToRemove && breadcrumbs.count) {
lastRemovedBreadcrumb = [breadcrumbs firstObject];
[breadcrumbs removeObjectAtIndex:0];

NSDictionary *dict = [lastRemovedBreadcrumb objectValue];
NSData *data = BSGJSONDataFromDictionary(dict, NULL);
bytesRemoved += data.length;
count++;
}

if (lastRemovedBreadcrumb) {
lastRemovedBreadcrumb.message = count < 2 ? @"Removed to reduce payload size" :
[NSString stringWithFormat:@"Removed, along with %lu older breadcrumb%s, to reduce payload size",
(unsigned long)(count - 1), count == 2 ? "" : "s"];
lastRemovedBreadcrumb.metadata = @{};
[breadcrumbs insertObject:lastRemovedBreadcrumb atIndex:0];
}

self.breadcrumbs = breadcrumbs;

NSDictionary *usage = self.usage;
if (usage) {
self.usage = BSGDictMerge(@{
@"system": @{
@"breadcrumbBytesRemoved": @(bytesRemoved),
@"breadcrumbsRemoved": @(count)}
}, usage);
}
}

- (void)truncateStrings:(NSUInteger)maxLength {
BSGTruncateContext context = {
.maxLength = maxLength
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog

### Enhancements

* Trim breadcrumb messages & metadata in payloads that exceed the size limit.
[#1451](https://github.com/bugsnag/bugsnag-cocoa/pull/1451)

* Truncate breadcrumb and metadata strings that are longer than `configuration.maxStringValueLength`.
[#1449](https://github.com/bugsnag/bugsnag-cocoa/pull/1449)

Expand Down
61 changes: 61 additions & 0 deletions Tests/BugsnagTests/BugsnagEventTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#import "BSG_RFC3339DateTool.h"
#import "Bugsnag.h"
#import "BugsnagBreadcrumb+Private.h"
#import "BugsnagClient+Private.h"
#import "BugsnagEvent+Private.h"
#import "BugsnagHandledState.h"
Expand Down Expand Up @@ -321,6 +322,66 @@ - (void)testJsonToEventToJson {
}
}

- (void)testTrimBreadcrumbs {
BugsnagEvent *event = [BugsnagEvent new];

BugsnagBreadcrumb * (^ MakeBreadcrumb)() = ^(BSGBreadcrumbType type, NSString *message, NSDictionary *metadata) {
BugsnagBreadcrumb *breadcrumb = [BugsnagBreadcrumb new];
breadcrumb.type = type;
breadcrumb.message = message;
breadcrumb.metadata = metadata;
return breadcrumb;
};

event.breadcrumbs = @[
MakeBreadcrumb(BSGBreadcrumbTypeState, @"Test started", @{}), // 91 bytes
MakeBreadcrumb(BSGBreadcrumbTypeLog, @"Some log message", @{@"some": @"metadata"}), // 110 bytes
MakeBreadcrumb(BSGBreadcrumbTypeManual, @"The final breadcrumb", @{@"key": @"untouched"})];

event.usage = @{@"sentinel": @42}; // Enable gathering telemetry

[event trimBreadcrumbs:100];

XCTAssertEqual(event.breadcrumbs.count, 2);

XCTAssertEqual (event.breadcrumbs[0].type, BSGBreadcrumbTypeLog);
XCTAssertEqualObjects(event.breadcrumbs[0].message, @"Removed, along with 1 older breadcrumb, to reduce payload size");
XCTAssertEqualObjects(event.breadcrumbs[0].metadata, @{});

XCTAssertEqual (event.breadcrumbs[1].type, BSGBreadcrumbTypeManual);
XCTAssertEqualObjects(event.breadcrumbs[1].message, @"The final breadcrumb");
XCTAssertEqualObjects(event.breadcrumbs[1].metadata, @{@"key": @"untouched"});

XCTAssertEqualObjects(event.usage, (@{@"system": @{@"breadcrumbBytesRemoved": @(91 + 110), @"breadcrumbsRemoved": @2}, @"sentinel": @42}));
}

- (void)testTrimSingleBreadcrumbs {
BugsnagEvent *event = [BugsnagEvent new];

BugsnagBreadcrumb *breadcrumb = [BugsnagBreadcrumb new];
breadcrumb.message = @""
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i"
"ncididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostru"
"d exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aut"
"e irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat n"
"ulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui"
" officia deserunt mollit anim id est laborum.";
breadcrumb.metadata = @{@"something": @"👍🏾🔥"};
breadcrumb.type = BSGBreadcrumbTypeError;
event.breadcrumbs = @[breadcrumb];

NSUInteger byteCount = [NSJSONSerialization dataWithJSONObject:[breadcrumb objectValue] options:0 error:NULL].length;

event.usage = @{}; // Enable gathering telemetry

[event trimBreadcrumbs:100];

XCTAssertEqual (event.breadcrumbs[0].type, BSGBreadcrumbTypeError);
XCTAssertEqualObjects(event.breadcrumbs[0].message, @"Removed to reduce payload size");
XCTAssertEqualObjects(event.breadcrumbs[0].metadata, @{});
XCTAssertEqualObjects(event.usage, (@{@"system": @{@"breadcrumbBytesRemoved": @(byteCount), @"breadcrumbsRemoved": @1}}));
}

- (void)testTruncateStrings {
BugsnagEvent *event = [BugsnagEvent new];

Expand Down
14 changes: 14 additions & 0 deletions features/delivery.feature
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,17 @@ Feature: Delivery of errors
Then the session "user.id" equals "3"
And I discard the oldest session
And the session "user.id" equals "2"

Scenario: Breadcrumbs should be trimmed if payload is oversized
When I run "OversizedBreadcrumbsScenario"
And I wait to receive an error
Then the event "breadcrumbs" is an array with 10 elements
And the error "Content-Length" header matches the regex "^9\d{5}$"
And the event "breadcrumbs.0.metaData.a" is null
And the event "breadcrumbs.0.name" equals "Removed, along with 16 older breadcrumbs, to reduce payload size"
And the event "breadcrumbs.9.metaData.a" is not null
And the event "breadcrumbs.9.name" equals "Breadcrumb 25"
And the event "usage.system.breadcrumbBytesRemoved" equals 1602740
And the event "usage.system.breadcrumbsRemoved" equals 17
And the event "usage.system.stringCharsTruncated" is not null
And the event "usage.system.stringsTruncated" is not null
4 changes: 4 additions & 0 deletions features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
01221E55282E5538008095C3 /* MaxPersistedSessionsScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01221E54282E5538008095C3 /* MaxPersistedSessionsScenario.m */; };
0163BFA72583B3CF008DC28B /* DiscardClassesHandledExceptionRegexScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BFA62583B3CF008DC28B /* DiscardClassesHandledExceptionRegexScenario.swift */; };
017B4134276B8D9B0054C91D /* OnSendErrorPersistenceScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 017B4133276B8D9B0054C91D /* OnSendErrorPersistenceScenario.m */; };
017BA42428A1558A00CB985E /* OversizedBreadcrumbsScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017BA42328A1558A00CB985E /* OversizedBreadcrumbsScenario.swift */; };
017DCFA028743FB5000ECB22 /* TelemetryUsageDisabledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017DCF9F28743FB5000ECB22 /* TelemetryUsageDisabledScenario.swift */; };
01847DD626453D4E00ADA4C7 /* InvalidCrashReportScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DD526453D4E00ADA4C7 /* InvalidCrashReportScenario.m */; };
01AF6A53258A112F00FFC803 /* BareboneTestHandledScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A52258A112F00FFC803 /* BareboneTestHandledScenario.swift */; };
Expand Down Expand Up @@ -252,6 +253,7 @@
01221E54282E5538008095C3 /* MaxPersistedSessionsScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MaxPersistedSessionsScenario.m; sourceTree = "<group>"; };
0163BFA62583B3CF008DC28B /* DiscardClassesHandledExceptionRegexScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardClassesHandledExceptionRegexScenario.swift; sourceTree = "<group>"; };
017B4133276B8D9B0054C91D /* OnSendErrorPersistenceScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnSendErrorPersistenceScenario.m; sourceTree = "<group>"; };
017BA42328A1558A00CB985E /* OversizedBreadcrumbsScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OversizedBreadcrumbsScenario.swift; sourceTree = "<group>"; };
017DCF9F28743FB5000ECB22 /* TelemetryUsageDisabledScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryUsageDisabledScenario.swift; sourceTree = "<group>"; };
01847DD526453D4E00ADA4C7 /* InvalidCrashReportScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InvalidCrashReportScenario.m; sourceTree = "<group>"; };
01AF6A52258A112F00FFC803 /* BareboneTestHandledScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BareboneTestHandledScenario.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -579,6 +581,7 @@
010BAAFC2833CE570003FF36 /* OOMWillTerminateScenario.m */,
E700EE58247D321B008CFFB6 /* OriginalErrorNSErrorScenario.swift */,
E700EE5A247D3224008CFFB6 /* OriginalErrorNSExceptionScenario.swift */,
017BA42328A1558A00CB985E /* OversizedBreadcrumbsScenario.swift */,
01F6B75C2832757F00B75C5D /* OversizedCrashReportScenario.swift */,
01F6B75D2832757F00B75C5D /* OversizedHandledErrorScenario.swift */,
F4295B041F9CC494473DD226 /* OverwriteLinkRegisterScenario.m */,
Expand Down Expand Up @@ -745,6 +748,7 @@
010BAB252833D0070003FF36 /* AppHangDisabledScenario.swift in Sources */,
E75040A02478019D005D33BD /* AutoDetectFalseHandledScenario.swift in Sources */,
01F115C927BAAF2D00892B1E /* SIGPIPEIgnoredScenario.m in Sources */,
017BA42428A1558A00CB985E /* OversizedBreadcrumbsScenario.swift in Sources */,
8AB8866620404DD30003E444 /* ViewController.swift in Sources */,
010BAB0E2833CE570003FF36 /* UnhandledMachExceptionOverrideScenario.m in Sources */,
8AB8866420404DD30003E444 /* AppDelegate.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
01AFCFC7282C058D00D48D45 /* OldSessionScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01AFCFC6282C058D00D48D45 /* OldSessionScenario.m */; };
01B6BB7225D56CBF00FC4DE6 /* LastRunInfoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenario.swift */; };
01B6BBA225DA774C00FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6BBA125DA774C00FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift */; };
01BB5D2628A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BB5D2528A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift */; };
01DCB82D279868160048640A /* ConcurrentCrashesScenario.mm in Sources */ = {isa = PBXBuildFile; fileRef = 01DCB82C279868160048640A /* ConcurrentCrashesScenario.mm */; };
01DE903A26CEAD1200455213 /* CriticalThermalStateScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DE903926CEAD1200455213 /* CriticalThermalStateScenario.swift */; };
01E0DB0625E8E95700A740ED /* AppDurationScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0DB0425E8E90500A740ED /* AppDurationScenario.swift */; };
Expand Down Expand Up @@ -269,6 +270,7 @@
01AFCFC6282C058D00D48D45 /* OldSessionScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OldSessionScenario.m; sourceTree = "<group>"; };
01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastRunInfoScenario.swift; sourceTree = "<group>"; };
01B6BBA125DA774C00FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendLaunchCrashesSynchronouslyScenario.swift; sourceTree = "<group>"; };
01BB5D2528A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OversizedBreadcrumbsScenario.swift; sourceTree = "<group>"; };
01DCB82C279868160048640A /* ConcurrentCrashesScenario.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ConcurrentCrashesScenario.mm; sourceTree = "<group>"; };
01DE903926CEAD1200455213 /* CriticalThermalStateScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CriticalThermalStateScenario.swift; sourceTree = "<group>"; };
01E0DB0425E8E90500A740ED /* AppDurationScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDurationScenario.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -530,6 +532,7 @@
01F47C97254B1B2F00B184AD /* OOMWillTerminateScenario.m */,
01F47C86254B1B2F00B184AD /* OriginalErrorNSErrorScenario.swift */,
01F47C21254B1B2C00B184AD /* OriginalErrorNSExceptionScenario.swift */,
01BB5D2528A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift */,
01F6B74B2832381300B75C5D /* OversizedCrashReportScenario.swift */,
01F6B7492832381300B75C5D /* OversizedHandledErrorScenario.swift */,
01F47CC0254B1B3000B184AD /* OverwriteLinkRegisterScenario.m */,
Expand Down Expand Up @@ -938,6 +941,7 @@
010BAB672833D34A0003FF36 /* HandledErrorValidReleaseStageScenario.swift in Sources */,
01F47CD1254B1B3100B184AD /* ManualContextClientScenario.swift in Sources */,
010BAB732833D34A0003FF36 /* AppAndDeviceAttributesStartWithApiKeyScenario.swift in Sources */,
01BB5D2628A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift in Sources */,
01F47CC9254B1B3100B184AD /* BuiltinTrapScenario.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class OversizedBreadcrumbsScenario: Scenario {

override func run() {

var metadata: [String: String] = [:]
for char in "abcdefghij" {
metadata["\(char)"] = String(repeating: ".", count: 10_000)
}

for i in 1...25 {
Bugsnag.leaveBreadcrumb("Breadcrumb \(i)", metadata: metadata, type: .navigation)
}

Bugsnag.notifyError(NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError))
}
}