From 513b85b306c6427ef558bff0abaa6ab5d99213d8 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 8 Aug 2022 15:42:56 +0100 Subject: [PATCH] Trim breadcrumbs in oversized payloads --- Bugsnag/Delivery/BSGEventUploadOperation.m | 38 +++++++++--- Bugsnag/Payload/BugsnagEvent+Private.h | 2 + Bugsnag/Payload/BugsnagEvent.m | 36 +++++++++++ CHANGELOG.md | 3 + Tests/BugsnagTests/BugsnagEventTests.m | 61 +++++++++++++++++++ features/delivery.feature | 14 +++++ .../ios/iOSTestApp.xcodeproj/project.pbxproj | 4 ++ .../macOSTestApp.xcodeproj/project.pbxproj | 4 ++ .../OversizedBreadcrumbsScenario.swift | 16 +++++ 9 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 features/fixtures/shared/scenarios/OversizedBreadcrumbsScenario.swift diff --git a/Bugsnag/Delivery/BSGEventUploadOperation.m b/Bugsnag/Delivery/BSGEventUploadOperation.m index 7ff7bee91..988dbe02d 100644 --- a/Bugsnag/Delivery/BSGEventUploadOperation.m +++ b/Bugsnag/Delivery/BSGEventUploadOperation.m @@ -76,12 +76,12 @@ - (void)runWithDelegate:(id)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]; @@ -97,8 +97,11 @@ - (void)runWithDelegate:(id)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]]; @@ -107,11 +110,6 @@ - (void)runWithDelegate:(id)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]; @@ -140,6 +138,26 @@ - (void)runWithDelegate:(id)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: @@ -149,7 +167,7 @@ - (void)runWithDelegate:(id)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: diff --git a/Bugsnag/Payload/BugsnagEvent+Private.h b/Bugsnag/Payload/BugsnagEvent+Private.h index c7f3c0095..d8d004dca 100644 --- a/Bugsnag/Payload/BugsnagEvent+Private.h +++ b/Bugsnag/Payload/BugsnagEvent+Private.h @@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)toJsonWithRedactedKeys:(nullable NSSet *)redactedKeys; +- (void)trimBreadcrumbs:(NSUInteger)bytesToRemove; + - (void)truncateStrings:(NSUInteger)maxLength; - (void)notifyUnhandledOverridden; diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 46559fe83..6d7fb3286 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -10,6 +10,7 @@ #import "BSGDefines.h" #import "BSGFeatureFlagStore.h" +#import "BSGJSONSerialization.h" #import "BSGKeys.h" #import "BSGSerialization.h" #import "BSGUtils.h" @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aaebdade..b9d90a5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Tests/BugsnagTests/BugsnagEventTests.m b/Tests/BugsnagTests/BugsnagEventTests.m index e163f74a1..b4dfdc201 100644 --- a/Tests/BugsnagTests/BugsnagEventTests.m +++ b/Tests/BugsnagTests/BugsnagEventTests.m @@ -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" @@ -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]; diff --git a/features/delivery.feature b/features/delivery.feature index 89f53ec80..d23409ea0 100644 --- a/features/delivery.feature +++ b/features/delivery.feature @@ -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 diff --git a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj index 94be40539..5ae5030bd 100644 --- a/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios/iOSTestApp.xcodeproj/project.pbxproj @@ -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 */; }; @@ -252,6 +253,7 @@ 01221E54282E5538008095C3 /* MaxPersistedSessionsScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MaxPersistedSessionsScenario.m; sourceTree = ""; }; 0163BFA62583B3CF008DC28B /* DiscardClassesHandledExceptionRegexScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardClassesHandledExceptionRegexScenario.swift; sourceTree = ""; }; 017B4133276B8D9B0054C91D /* OnSendErrorPersistenceScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnSendErrorPersistenceScenario.m; sourceTree = ""; }; + 017BA42328A1558A00CB985E /* OversizedBreadcrumbsScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OversizedBreadcrumbsScenario.swift; sourceTree = ""; }; 017DCF9F28743FB5000ECB22 /* TelemetryUsageDisabledScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryUsageDisabledScenario.swift; sourceTree = ""; }; 01847DD526453D4E00ADA4C7 /* InvalidCrashReportScenario.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InvalidCrashReportScenario.m; sourceTree = ""; }; 01AF6A52258A112F00FFC803 /* BareboneTestHandledScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BareboneTestHandledScenario.swift; sourceTree = ""; }; @@ -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 */, @@ -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 */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index e0faffbb7..f3bb18aa6 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -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 */; }; @@ -269,6 +270,7 @@ 01AFCFC6282C058D00D48D45 /* OldSessionScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OldSessionScenario.m; sourceTree = ""; }; 01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastRunInfoScenario.swift; sourceTree = ""; }; 01B6BBA125DA774C00FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendLaunchCrashesSynchronouslyScenario.swift; sourceTree = ""; }; + 01BB5D2528A1463C00A7F322 /* OversizedBreadcrumbsScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OversizedBreadcrumbsScenario.swift; sourceTree = ""; }; 01DCB82C279868160048640A /* ConcurrentCrashesScenario.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ConcurrentCrashesScenario.mm; sourceTree = ""; }; 01DE903926CEAD1200455213 /* CriticalThermalStateScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CriticalThermalStateScenario.swift; sourceTree = ""; }; 01E0DB0425E8E90500A740ED /* AppDurationScenario.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDurationScenario.swift; sourceTree = ""; }; @@ -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 */, @@ -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; diff --git a/features/fixtures/shared/scenarios/OversizedBreadcrumbsScenario.swift b/features/fixtures/shared/scenarios/OversizedBreadcrumbsScenario.swift new file mode 100644 index 000000000..12878e359 --- /dev/null +++ b/features/fixtures/shared/scenarios/OversizedBreadcrumbsScenario.swift @@ -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)) + } +}