diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index 28984bfe3..082d79853 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -723,6 +723,10 @@ 01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; }; 01E8765F256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; }; 01E87660256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; }; + 01F9FCB628929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; }; + 01F9FCB728929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; }; + 01F9FCB828929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; }; + 01F9FCB928929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; }; 3A700A9424A63ABC0068CD1B /* BugsnagThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8024A63A8E0068CD1B /* BugsnagThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3A700A9524A63AC50068CD1B /* BugsnagSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8124A63A8E0068CD1B /* BugsnagSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3A700A9624A63AC60068CD1B /* BugsnagStackframe.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8224A63A8E0068CD1B /* BugsnagStackframe.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1628,6 +1632,7 @@ 01DE903B26CEAF9E00455213 /* BSGUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGUtilsTests.m; sourceTree = ""; }; 01E8765C256684E700F4B70A /* URLSessionMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = URLSessionMock.h; sourceTree = ""; }; 01E8765D256684E700F4B70A /* URLSessionMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLSessionMock.m; sourceTree = ""; }; + 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGSerializationTests.m; sourceTree = ""; }; 3A700A8024A63A8E0068CD1B /* BugsnagThread.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagThread.h; sourceTree = ""; }; 3A700A8124A63A8E0068CD1B /* BugsnagSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagSession.h; sourceTree = ""; }; 3A700A8224A63A8E0068CD1B /* BugsnagStackframe.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagStackframe.h; sourceTree = ""; }; @@ -2033,6 +2038,7 @@ 0163BF5825823D8D008DC28B /* BSGNotificationBreadcrumbsTests.m */, 008966C82486D43600DC48C2 /* BSGOutOfMemoryTests.m */, 0130DEF82880203A00E5953F /* BSGRunContextTests.m */, + 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */, CB6419AA25A73E8C00613D25 /* BSGStorageMigratorTests.m */, 017DCF9A287422BB000ECB22 /* BSGTelemetryTests.m */, 01DE903B26CEAF9E00455213 /* BSGUtilsTests.m */, @@ -3236,6 +3242,7 @@ CBA2249B251E429C00B87416 /* TestSupport.m in Sources */, 008967332486D43700DC48C2 /* BugsnagClientTests.m in Sources */, 004E353F2487B3BD007FBAE4 /* BugsnagSwiftConfigurationTests.swift in Sources */, + 01F9FCB628929336005EDD8C /* BSGSerializationTests.m in Sources */, 008967542486D43700DC48C2 /* BugsnagOnCrashTest.m in Sources */, 008967152486D43700DC48C2 /* BugsnagCollectionsTests.m in Sources */, 01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */, @@ -3395,6 +3402,7 @@ 0089671C2486D43700DC48C2 /* BugsnagSessionTest.m in Sources */, 008967AC2486D43700DC48C2 /* BSG_KSMachTests.m in Sources */, 0163BF5A25823D8D008DC28B /* BSGNotificationBreadcrumbsTests.m in Sources */, + 01F9FCB728929336005EDD8C /* BSGSerializationTests.m in Sources */, 01BDB1FD25DEBFB300A91FAF /* BSGEventUploadKSCrashReportOperationTests.m in Sources */, 00896A452486DBF000DC48C2 /* BugsnagConfigurationTests.m in Sources */, 008967492486D43700DC48C2 /* BugsnagUserTest.m in Sources */, @@ -3543,6 +3551,7 @@ E701FAAD2490EFD9008D842F /* EventApiValidationTest.m in Sources */, 008967472486D43700DC48C2 /* BugsnagTests.m in Sources */, 010993A6273D188B00128BBE /* BSGFeatureFlagStoreTests.m in Sources */, + 01F9FCB828929336005EDD8C /* BSGSerializationTests.m in Sources */, 008967A72486D43700DC48C2 /* KSString_Tests.m in Sources */, 0089671A2486D43700DC48C2 /* BugsnagErrorTest.m in Sources */, 008967172486D43700DC48C2 /* BugsnagCollectionsTests.m in Sources */, @@ -3843,6 +3852,7 @@ CB28F0A228294D4F003AB200 /* KSCrashReportWriterTests.m in Sources */, CB28F0D3282A4B91003AB200 /* BugsnagErrorTest.m in Sources */, CB28F0DE282A4BEE003AB200 /* BugsnagSessionTrackerStopTest.m in Sources */, + 01F9FCB928929336005EDD8C /* BSGSerializationTests.m in Sources */, CB28F0B328294DD0003AB200 /* BSGClientObserverTests.m in Sources */, CB28F0B128294D4F003AB200 /* KSCrashSentry_Tests.m in Sources */, CB28F0B228294D52003AB200 /* XCTestCase+KSCrash.m in Sources */, diff --git a/Bugsnag/Breadcrumbs/BugsnagBreadcrumbs.h b/Bugsnag/Breadcrumbs/BugsnagBreadcrumbs.h index 1657d4b64..3a0e8fdde 100644 --- a/Bugsnag/Breadcrumbs/BugsnagBreadcrumbs.h +++ b/Bugsnag/Breadcrumbs/BugsnagBreadcrumbs.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithConfiguration:(BugsnagConfiguration *)config; /** - * The breadcrumbs stored in memory. + * Returns an array of new objects representing the breadcrumbs stored in memory. */ @property (readonly, nonatomic) NSArray *breadcrumbs; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 5f4c93b8c..a69d3e8a2 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -94,6 +94,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { [copy setSendLaunchCrashesSynchronously:self.sendLaunchCrashesSynchronously]; [copy setMaxPersistedEvents:self.maxPersistedEvents]; [copy setMaxPersistedSessions:self.maxPersistedSessions]; + [copy setMaxStringValueLength:self.maxStringValueLength]; [copy setMaxBreadcrumbs:self.maxBreadcrumbs]; [copy setNotifier:self.notifier]; [copy setFeatureFlagStore:self.featureFlagStore]; @@ -189,6 +190,7 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { _maxBreadcrumbs = 50; _maxPersistedEvents = 32; _maxPersistedSessions = 128; + _maxStringValueLength = 10000; _autoTrackSessions = YES; #if BSG_HAVE_MACH_THREADS _sendThreads = BSGThreadSendPolicyAlways; diff --git a/Bugsnag/Delivery/BSGEventUploadOperation.m b/Bugsnag/Delivery/BSGEventUploadOperation.m index 33b047ca4..3c9e4b002 100644 --- a/Bugsnag/Delivery/BSGEventUploadOperation.m +++ b/Bugsnag/Delivery/BSGEventUploadOperation.m @@ -95,9 +95,13 @@ - (void)runWithDelegate:(id)delegate completion NSDictionary *eventPayload; @try { + [event truncateStrings:configuration.maxStringValueLength]; eventPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys]; } @catch (NSException *exception) { bsg_log_err(@"Discarding event %@ because an exception was thrown by -toJsonWithRedactedKeys: %@", self.name, exception); + [BSGInternalErrorReporter.sharedInstance reportException:exception diagnostics:nil groupingHash: + [NSString stringWithFormat:@"BSGEventUploadOperation -[runWithDelegate:completionHandler:] %@ %@", + exception.name, exception.reason]]; [self deleteEvent]; completionHandler(); return; diff --git a/Bugsnag/Helpers/BSGSerialization.h b/Bugsnag/Helpers/BSGSerialization.h index 70047c173..0f3c85d0f 100644 --- a/Bugsnag/Helpers/BSGSerialization.h +++ b/Bugsnag/Helpers/BSGSerialization.h @@ -1,40 +1,32 @@ -#ifndef BugsnagJSONSerializable_h -#define BugsnagJSONSerializable_h - #import -/** - Removes any values which would be rejected by NSJSONSerialization for - documented reasons - - @param input an array - @return a new array - */ -NSArray *_Nonnull BSGSanitizeArray(NSArray *_Nonnull input); +NS_ASSUME_NONNULL_BEGIN /** Removes any values which would be rejected by NSJSONSerialization for - documented reasons + documented reasons or is NSNull @param input a dictionary @return a new dictionary */ -NSDictionary *_Nonnull BSGSanitizeDict(NSDictionary *_Nonnull input); - -/** - Checks whether the base type would be accepted by the serialization process - - @param obj any object or nil - @return YES if the object is an Array, Dictionary, String, Number, or NSNull - */ -BOOL BSGIsSanitizedType(id _Nullable obj); +NSMutableDictionary * BSGSanitizeDict(NSDictionary *input); /** Cleans the object, including nested dictionary and array values @param obj any object or nil - @return a new object for serialization or nil if the obj was incompatible + @return a new object for serialization or nil if the obj was incompatible or NSNull */ id _Nullable BSGSanitizeObject(id _Nullable obj); -#endif +typedef struct _BSGTruncateContext { + NSUInteger maxLength; + NSUInteger strings; + NSUInteger length; +} BSGTruncateContext; + +NSString * BSGTruncateString(BSGTruncateContext *context, NSString *string); + +id BSGTruncateStrings(BSGTruncateContext *context, id object); + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Helpers/BSGSerialization.m b/Bugsnag/Helpers/BSGSerialization.m index 64751c377..135611ef1 100644 --- a/Bugsnag/Helpers/BSGSerialization.m +++ b/Bugsnag/Helpers/BSGSerialization.m @@ -1,41 +1,26 @@ #import "BSGSerialization.h" -#import "BugsnagLogger.h" + #import "BSGJSONSerialization.h" +#import "BugsnagLogger.h" -BOOL BSGIsSanitizedType(id obj) { - static dispatch_once_t onceToken; - static NSArray *allowedTypes = nil; - dispatch_once(&onceToken, ^{ - allowedTypes = @[ - [NSArray class], [NSDictionary class], [NSNull class], - [NSNumber class], [NSString class] - ]; - }); - - for (Class klass in allowedTypes) { - if ([obj isKindOfClass:klass]) - return YES; - } - return NO; -} +static NSArray * BSGSanitizeArray(NSArray *input); id BSGSanitizeObject(id obj) { - if ([obj isKindOfClass:[NSNumber class]]) { - NSNumber *number = obj; - if (![number isEqualToNumber:[NSDecimalNumber notANumber]] && - !isinf([number doubleValue])) - return obj; - } else if ([obj isKindOfClass:[NSArray class]]) { + if ([obj isKindOfClass:[NSArray class]]) { return BSGSanitizeArray(obj); } else if ([obj isKindOfClass:[NSDictionary class]]) { return BSGSanitizeDict(obj); - } else if (BSGIsSanitizedType(obj)) { + } else if ([obj isKindOfClass:[NSString class]]) { + return obj; + } else if ([obj isKindOfClass:[NSNumber class]] + && ![obj isEqualToNumber:[NSDecimalNumber notANumber]] + && !isinf([obj doubleValue])) { return obj; } return nil; } -NSDictionary *_Nonnull BSGSanitizeDict(NSDictionary *input) { +NSMutableDictionary * BSGSanitizeDict(NSDictionary *input) { __block NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:[input count]]; [input enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, @@ -49,7 +34,7 @@ id BSGSanitizeObject(id obj) { return output; } -NSArray *BSGSanitizeArray(NSArray *input) { +static NSArray * BSGSanitizeArray(NSArray *input) { NSMutableArray *output = [NSMutableArray arrayWithCapacity:[input count]]; for (id obj in input) { id cleanedObject = BSGSanitizeObject(obj); @@ -58,3 +43,37 @@ id BSGSanitizeObject(id obj) { } return output; } + +NSString * BSGTruncateString(BSGTruncateContext *context, NSString *string) { + const NSUInteger inputLength = string.length; + if (inputLength <= context->maxLength) return string; + // Prevent chopping in the middle of a composed character sequence + NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:context->maxLength]; + NSString *output = [string substringToIndex:range.location]; + NSUInteger count = inputLength - range.location; + context->strings++; + context->length += count; + return [output stringByAppendingFormat:@"\n***%lu CHARS TRUNCATED***", (unsigned long)count]; +} + +id BSGTruncateStrings(BSGTruncateContext *context, id object) { + if ([object isKindOfClass:[NSString class]]) { + return BSGTruncateString(context, object); + } + if ([object isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:((NSDictionary *)object).count]; + for (NSString *key in (NSDictionary *)object) { + id value = ((NSDictionary *)object)[key]; + output[key] = BSGTruncateStrings(context, value); + } + return output; + } + if ([object isKindOfClass:[NSArray class]]) { + NSMutableArray *output = [NSMutableArray arrayWithCapacity:((NSArray *)object).count]; + for (id element in (NSArray *)object) { + [output addObject:BSGTruncateStrings(context, element)]; + } + return output; + } + return object; +} diff --git a/Bugsnag/Metadata/BugsnagMetadata.m b/Bugsnag/Metadata/BugsnagMetadata.m index 092fc2856..88b56f2a6 100644 --- a/Bugsnag/Metadata/BugsnagMetadata.m +++ b/Bugsnag/Metadata/BugsnagMetadata.m @@ -56,7 +56,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict { if ((self = [super init])) { // Ensure that the instantiating dictionary is mutable. // Saves checks later. - _dictionary = [self sanitizeDictionary:dict]; + _dictionary = BSGSanitizeDict(dict); self.stateEventBlocks = [NSMutableArray new]; } if (self.observer) { @@ -65,44 +65,6 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict { return self; } -/** - * Sanitizes the given dictionary to prevent [NSNull null] values from being added - * to the metadata when deserializing a payload. - * - * @param dictionary the input dictionary - * @return a sanitized dictionary - */ -- (NSMutableDictionary *)sanitizeDictionary:(NSDictionary *)dictionary { - NSMutableDictionary *input = [dictionary mutableCopy]; - - for (NSString *key in [input allKeys]) { - id obj = input[key]; - - if (obj == [NSNull null]) { - [input removeObjectForKey:key]; - } else if ([obj isKindOfClass:[NSDictionary class]]) { - input[key] = [self sanitizeDictionary:obj]; - } else if ([obj isKindOfClass:[NSArray class]]) { - input[key] = [self sanitizeArray:obj]; - } - } - return input; -} - -- (NSMutableArray *)sanitizeArray:(NSArray *)obj { - NSMutableArray *ary = [obj mutableCopy]; - [ary removeObject:[NSNull null]]; - - for (NSUInteger k = 0; k < [ary count]; ++k) { - if ([ary[k] isKindOfClass:[NSDictionary class]]) { - ary[k] = [self sanitizeDictionary:ary[k]]; - } else if ([ary[k] isKindOfClass:[NSArray class]]) { - ary[k] = [self sanitizeArray:ary[k]]; - } - } - return ary; -} - - (NSDictionary *)toDictionary { @synchronized (self) { return [self.dictionary mutableCopy]; diff --git a/Bugsnag/Payload/BugsnagEvent+Private.h b/Bugsnag/Payload/BugsnagEvent+Private.h index c822eac74..c7f3c0095 100644 --- a/Bugsnag/Payload/BugsnagEvent+Private.h +++ b/Bugsnag/Payload/BugsnagEvent+Private.h @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN /// An array of string representations of BSGErrorType describing the types of stackframe / stacktrace in this error. @property (readonly, nonatomic) NSArray *stacktraceTypes; -/// Usage telemetry info, from BSGTelemetryCreateUsage() +/// Usage telemetry info, from BSGTelemetryCreateUsage(), or nil if BSGTelemetryUsage is not enabled. @property (readwrite, nullable, nonatomic) NSDictionary *usage; @property (readwrite, nonnull, nonatomic) BugsnagUser *user; @@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSDictionary *)toJsonWithRedactedKeys:(nullable NSSet *)redactedKeys; +- (void)truncateStrings:(NSUInteger)maxLength; + - (void)notifyUnhandledOverridden; @end diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 76b219580..46559fe83 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -8,6 +8,7 @@ #import "BugsnagEvent+Private.h" +#import "BSGDefines.h" #import "BSGFeatureFlagStore.h" #import "BSGKeys.h" #import "BSGSerialization.h" @@ -31,7 +32,6 @@ #import "BugsnagStacktrace.h" #import "BugsnagThread+Private.h" #import "BugsnagUser+Private.h" -#import "BSGDefines.h" static NSString * const RedactedMetadataValue = @"[REDACTED]"; @@ -690,6 +690,32 @@ - (void)symbolicateIfNeeded { } } +- (void)truncateStrings:(NSUInteger)maxLength { + BSGTruncateContext context = { + .maxLength = maxLength + }; + + for (BugsnagBreadcrumb *breadcrumb in self.breadcrumbs) { + breadcrumb.message = BSGTruncateString(&context, breadcrumb.message); + breadcrumb.metadata = BSGTruncateStrings(&context, breadcrumb.metadata); + } + + BugsnagMetadata *metadata = self.metadata; + if (metadata) { + self.metadata = [[BugsnagMetadata alloc] initWithDictionary: + BSGTruncateStrings(&context, metadata.dictionary)]; + } + + NSDictionary *usage = self.usage; + if (usage) { + self.usage = BSGDictMerge(@{ + @"system": @{ + @"stringCharsTruncated": @(context.length), + @"stringsTruncated": @(context.strings)} + }, usage); + } +} + - (BOOL)unhandled { return self.handledState.unhandled; } diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index 60b921451..7bdf59201 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -334,6 +334,15 @@ BUGSNAG_EXTERN */ @property (nonatomic) NSUInteger maxBreadcrumbs; +/** + * The maximum length of breadcrumb messages and metadata string values. + * + * Values longer than this will be truncated prior to sending, after running any OnSendError blocks. + * + * The default value is 10000. + */ +@property (nonatomic) NSUInteger maxStringValueLength; + /** * Whether User information should be persisted to disk between application runs. * Defaults to True. diff --git a/CHANGELOG.md b/CHANGELOG.md index 829ee6a1c..5aaebdade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Changelog ### Enhancements +* Truncate breadcrumb and metadata strings that are longer than `configuration.maxStringValueLength`. + [#1449](https://github.com/bugsnag/bugsnag-cocoa/pull/1449) + * Add `+[BugsnagStackframe stackframesWithCallStackReturnAddresses:]` to public headers. [#1446](https://github.com/bugsnag/bugsnag-cocoa/pull/1446) diff --git a/Tests/BugsnagTests/BSGSerializationTests.m b/Tests/BugsnagTests/BSGSerializationTests.m new file mode 100644 index 000000000..c36621781 --- /dev/null +++ b/Tests/BugsnagTests/BSGSerializationTests.m @@ -0,0 +1,97 @@ +// +// BSGSerializationTests.m +// Bugsnag +// +// Created by Nick Dowell on 28/07/2022. +// Copyright © 2022 Bugsnag Inc. All rights reserved. +// + +#import + +#import "BSGSerialization.h" + +@interface BSGSerializationTests : XCTestCase + +@end + +@implementation BSGSerializationTests + +- (void)testSanitizeObject { + XCTAssertEqualObjects(BSGSanitizeObject(@""), @""); + XCTAssertEqualObjects(BSGSanitizeObject(@42), @42); + XCTAssertEqualObjects(BSGSanitizeObject(@[@42]), @[@42]); + XCTAssertEqualObjects(BSGSanitizeObject(@[self]), @[]); + XCTAssertEqualObjects(BSGSanitizeObject(@{@"a": @"b"}), @{@"a": @"b"}); + XCTAssertEqualObjects(BSGSanitizeObject(@{@"self": self}), @{}); + XCTAssertNil(BSGSanitizeObject(@(INFINITY))); + XCTAssertNil(BSGSanitizeObject(@(NAN))); + XCTAssertNil(BSGSanitizeObject([NSDate date])); + XCTAssertNil(BSGSanitizeObject([NSDecimalNumber notANumber])); + XCTAssertNil(BSGSanitizeObject([NSNull null])); + XCTAssertNil(BSGSanitizeObject(self)); +} + +- (void)testTruncateString { + BSGTruncateContext context = {0}; + + context.maxLength = NSUIntegerMax; + XCTAssertEqualObjects(BSGTruncateString(&context, @"Hello, world!"), @"Hello, world!"); + XCTAssertEqual(context.strings, 0); + XCTAssertEqual(context.length, 0); + + context.maxLength = 5; + XCTAssertEqualObjects(BSGTruncateString(&context, @"Hello, world!"), @"Hello" + "\n***8 CHARS TRUNCATED***"); + XCTAssertEqual(context.strings, 1); + XCTAssertEqual(context.length, 8); + + // Verify that emoji (composed character sequences) are not partially truncated + // Note when adding tests that older OSes like iOS 9 don't understand more recently + // added emoji like 🏴󠁧󠁢󠁥󠁮󠁧󠁿 and 👩🏾‍🚀 and therefore won't be able to avoid slicing them. + + context.maxLength = 10; + XCTAssertEqualObjects(BSGTruncateString(&context, @"Emoji: 👍🏾"), @"Emoji: " + "\n***4 CHARS TRUNCATED***"); + XCTAssertEqual(context.strings, 2); + XCTAssertEqual(context.length, 12); +} + +- (void)testTruncateStringsWithString { + BSGTruncateContext context = (BSGTruncateContext){.maxLength = 3}; + XCTAssertEqualObjects(BSGTruncateStrings(&context, @"foo bar"), @"foo" + "\n***4 CHARS TRUNCATED***"); + XCTAssertEqual(context.strings, 1); + XCTAssertEqual(context.length, 4); +} + +- (void)testTruncateStringsWithArray { + BSGTruncateContext context = (BSGTruncateContext){.maxLength = 3}; + XCTAssertEqualObjects(BSGTruncateStrings(&context, @[@"foo bar"]), + @[@"foo" + "\n***4 CHARS TRUNCATED***"]); + XCTAssertEqual(context.strings, 1); + XCTAssertEqual(context.length, 4); +} + +- (void)testTruncateStringsWithObject { + BSGTruncateContext context = (BSGTruncateContext){.maxLength = 3}; + XCTAssertEqualObjects(BSGTruncateStrings(&context, @{@"name": @"foo bar"}), + @{@"name": @"foo" + "\n***4 CHARS TRUNCATED***"}); + XCTAssertEqual(context.strings, 1); + XCTAssertEqual(context.length, 4); +} + +- (void)testTruncateStringsWithNestedObjects { + BSGTruncateContext context = (BSGTruncateContext){.maxLength = 3}; + XCTAssertEqualObjects(BSGTruncateStrings(&context, (@{@"one": @{@"key": @"foo bar"}, + @"two": @{@"foo": @"Baa, Baa, Black Sheep"}})), + (@{@"one": @{@"key": @"foo" + "\n***4 CHARS TRUNCATED***"}, + @"two": @{@"foo": @"Baa" + "\n***18 CHARS TRUNCATED***"}})); + XCTAssertEqual(context.strings, 2); + XCTAssertEqual(context.length, 22); +} + +@end diff --git a/Tests/BugsnagTests/BugsnagConfigurationTests.m b/Tests/BugsnagTests/BugsnagConfigurationTests.m index ed9c31937..2f7108f02 100644 --- a/Tests/BugsnagTests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagTests/BugsnagConfigurationTests.m @@ -650,6 +650,7 @@ - (void)testDefaultConfigurationValues { XCTAssertEqualObjects(@"https://notify.bugsnag.com", config.endpoints.notify); XCTAssertEqualObjects(@"https://sessions.bugsnag.com", config.endpoints.sessions); XCTAssertEqual(50, config.maxBreadcrumbs); + XCTAssertEqual(config.maxStringValueLength, 10000); XCTAssertTrue(config.persistUser); XCTAssertEqual(1, [config.redactedKeys count]); XCTAssertEqualObjects(@"password", [config.redactedKeys allObjects][0]); @@ -872,6 +873,7 @@ - (void)testNSCopying { #if !TARGET_OS_WATCH [config setSendThreads:BSGThreadSendPolicyUnhandledOnly]; #endif + [config setMaxStringValueLength:100]; [config addPlugin:(id)[NSNull null]]; BugsnagOnSendErrorBlock onSendBlock1 = ^BOOL(BugsnagEvent * _Nonnull event) { return true; }; @@ -928,6 +930,8 @@ - (void)testNSCopying { // Plugins XCTAssert([clone.plugins containsObject:[NSNull null]]); XCTAssertNoThrow([clone.plugins removeObject:[NSNull null]]); + + XCTAssertEqual(clone.maxStringValueLength, 100); } - (void)testMetadataMutability { diff --git a/Tests/BugsnagTests/BugsnagEventTests.m b/Tests/BugsnagTests/BugsnagEventTests.m index 0e337604d..e163f74a1 100644 --- a/Tests/BugsnagTests/BugsnagEventTests.m +++ b/Tests/BugsnagTests/BugsnagEventTests.m @@ -321,6 +321,58 @@ - (void)testJsonToEventToJson { } } +- (void)testTruncateStrings { + BugsnagEvent *event = [BugsnagEvent new]; + + BugsnagBreadcrumb * (^ MakeBreadcrumb)() = ^(NSString *message) { + BugsnagBreadcrumb *breadcrumb = [BugsnagBreadcrumb new]; + breadcrumb.message = message; + breadcrumb.metadata = @{@"string": message}; + return breadcrumb; + }; + + event.breadcrumbs = @[ + MakeBreadcrumb(@"Lorem ipsum dolor si" + "t amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."), + + MakeBreadcrumb(@"Lorem ipsum is place" + "holder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups."), + + MakeBreadcrumb(@"20 characters string")]; + + event.metadata = [[BugsnagMetadata alloc] initWithDictionary:@{}]; + [event addMetadata:@"From its medieval or" + "igins to the digital era, learn everything there is to know about the ubiquitous lorem ipsum passage." + withKey:@"name" toSection:@"test"]; + + event.usage = @{}; // Enable gathering telemetry + + [event truncateStrings:20]; + + XCTAssertEqualObjects([event.usage valueForKeyPath:@"system.stringsTruncated"], @5); + + XCTAssertEqualObjects([event.usage valueForKeyPath:@"system.stringCharsTruncated"], @(103 + 103 + 117 + 117 + 101)); + + XCTAssertEqualObjects(event.breadcrumbs[0].message, @"Lorem ipsum dolor si" + "\n***103 CHARS TRUNCATED***"); + + XCTAssertEqualObjects(event.breadcrumbs[0].metadata[@"string"], @"Lorem ipsum dolor si" + "\n***103 CHARS TRUNCATED***"); + + XCTAssertEqualObjects(event.breadcrumbs[1].message, @"Lorem ipsum is place" + "\n***117 CHARS TRUNCATED***"); + + XCTAssertEqualObjects(event.breadcrumbs[1].metadata[@"string"], @"Lorem ipsum is place" + "\n***117 CHARS TRUNCATED***"); + + XCTAssertEqualObjects(event.breadcrumbs[2].message, @"20 characters string"); + + XCTAssertEqualObjects(event.breadcrumbs[2].metadata[@"string"], @"20 characters string"); + + XCTAssertEqualObjects([event getMetadataFromSection:@"test" withKey:@"name"], @"From its medieval or" + "\n***101 CHARS TRUNCATED***"); +} + // MARK: - Feature flags interface - (void)testFeatureFlags { @@ -679,4 +731,5 @@ - (void)testRuntimeVersionsUnhandled { }; XCTAssertEqualObjects(expected, event.device.runtimeVersions); } + @end diff --git a/features/barebone_tests.feature b/features/barebone_tests.feature index ddb4dda96..d041456e1 100644 --- a/features/barebone_tests.feature +++ b/features/barebone_tests.feature @@ -63,6 +63,7 @@ Feature: Barebone tests And the event "metaData.Exception.info" equals "Some error specific information" And the event "metaData.Flags.Testing" is true And the event "metaData.Other.password" equals "[REDACTED]" + And the event "metaData.Other.shouldBeTruncated" matches "\n\*\*\*345 CHARS TRUNCATED\*\*\*" And the event "metaData.error.nsexception.name" equals "NSRangeException" And the event "metaData.error.nsexception.userInfo.date" equals "2001-01-01 00:00:00 +0000" And the event "metaData.error.nsexception.userInfo.NSUnderlyingError" matches "Error Domain=ErrorDomain Code=0" @@ -88,6 +89,8 @@ Feature: Barebone tests | ios | true | | macos | @null | | watchos | @null | + And the event "usage.system.stringCharsTruncated" equals 345 + And the event "usage.system.stringsTruncated" equals 1 And the event "user.email" equals "foobar@example.com" And the event "user.id" equals "foobar" And the event "user.name" equals "Foo Bar" diff --git a/features/fixtures/shared/scenarios/BareboneTestHandledScenario.swift b/features/fixtures/shared/scenarios/BareboneTestHandledScenario.swift index b6a06cc58..763a83822 100644 --- a/features/fixtures/shared/scenarios/BareboneTestHandledScenario.swift +++ b/features/fixtures/shared/scenarios/BareboneTestHandledScenario.swift @@ -42,6 +42,7 @@ class BareboneTestHandledScenario: Scenario { config.addMetadata(["Testing": true], section: "Flags") config.addMetadata(["password": "123456"], section: "Other") config.launchDurationMillis = 0 + config.maxStringValueLength = 100 #if !os(watchOS) config.sendThreads = .unhandledOnly #endif @@ -78,6 +79,15 @@ class BareboneTestHandledScenario: Scenario { self.afterSendErrorBlock = self.afterSendError + Bugsnag.addMetadata(""" + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod \ + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, \ + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. \ + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu \ + fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in \ + culpa qui officia deserunt mollit anim id est laborum. + """, key: "shouldBeTruncated", section: "Other") + Bugsnag.notify(NSException(name: .rangeException, reason: "-[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]", userInfo: ["date": Date(timeIntervalSinceReferenceDate: 0), diff --git a/features/fixtures/shared/scenarios/OversizedCrashReportScenario.swift b/features/fixtures/shared/scenarios/OversizedCrashReportScenario.swift index a1478eaf1..57dfd6e78 100644 --- a/features/fixtures/shared/scenarios/OversizedCrashReportScenario.swift +++ b/features/fixtures/shared/scenarios/OversizedCrashReportScenario.swift @@ -3,6 +3,7 @@ class OversizedCrashReportScenario: Scenario { override func startBugsnag() { config.autoTrackSessions = false config.enabledErrorTypes.ooms = false + config.maxStringValueLength = UInt.max config.addOnSendError { var data = Data(count: 1024 * 1024) _ = data.withUnsafeMutableBytes { diff --git a/features/fixtures/shared/scenarios/OversizedHandledErrorScenario.swift b/features/fixtures/shared/scenarios/OversizedHandledErrorScenario.swift index efd783a24..414e6251c 100644 --- a/features/fixtures/shared/scenarios/OversizedHandledErrorScenario.swift +++ b/features/fixtures/shared/scenarios/OversizedHandledErrorScenario.swift @@ -3,6 +3,7 @@ class OversizedHandledErrorScenario: Scenario { override func startBugsnag() { config.autoTrackSessions = false config.enabledErrorTypes.ooms = false + config.maxStringValueLength = UInt.max config.addOnSendError { var data = Data(count: 1024 * 1024) _ = data.withUnsafeMutableBytes {