Skip to content

Commit

Permalink
feat: attributes (#14)
Browse files Browse the repository at this point in the history
* add attribute and value API. Update BugSplatTest-SwiftUI example app to use attribute and value API

* updated copyright, removed author

* chore: remove year from copyright

To match the pattern we follow in other repos

* fix: remove extra attribute keys

* chore: fix typo in sample

---------

Co-authored-by: David Ferrero <david.ferrero@zion.com>
  • Loading branch information
bobbyg603 and ferrerod authored Feb 13, 2025
1 parent f3099ff commit ae201ec
Show file tree
Hide file tree
Showing 21 changed files with 547 additions and 30 deletions.
35 changes: 35 additions & 0 deletions BugSplat.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,41 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, assign) BOOL autoSubmitCrashReport;

/**
* Add an attribute and value to the crash report.
* Attributes and values represent app supplied keys and values to associate with a crash report.
* Attributes and values will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8".
*
* IMPORTANT: For iOS, only one BugSplatAttachment is currently supported.
* If BugSplatDelegate's method `- (BugSplatAttachment *)attachmentForBugSplat:(BugSplat *)bugSplat` returns a non-nil BugSplatAttachment,
* BugSplat will send that BugSplatAttachment, not the BugSplatAttachment that would otherwise be created due to adding attributes and values using this method.
*
* NOTES:
*
* This method may be called multiple times, once per attribute+value pair.
* Attributes are backed by an NSDictionary so attribute names must be unique.
* If the attribute does not exist, it will be added to attributes dictionary.
* If attribute already exists, the value will be replaced in the dictionary.
* If attribute already exists, and the value is nil, the attribute will be removed from the dictionary.
*
* When this method is called, the following preprocessing occurs:
* 1. attribute will first have white space and newlines removed from both the beginning and end of the String.
*
* 2. attribute will then be processed by an XML escaping routine which looks for escapable characters ",',&,<, and >
* See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents
* Any XML comment blocks or CDATA blocks found will disable XML escaping within the block.
*
* 3. values will then be processed by an XML escaping routine which looks for escapable characters ",',&,<, and >
* Any XML comment blocks or CDATA blocks found will disable XML escaping within the block.
*
* 4. After processing both attribute and value for XML escape characters, the attribute+value pair will be stored in an NSDictionary.
*
* If a crash occurs, attributes and values will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml"
* and encoding of "UTF-8". The attachment will be included with the crash data (except as noted above regarding iOS BugSplatAttachment limitation).
*
*/
- (void)setValue:(nullable NSString *)value forAttribute:(NSString *)attribute;

// macOS specific API
#if TARGET_OS_OSX
/*!
Expand Down
125 changes: 116 additions & 9 deletions BugSplat.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@

#import <BugSplat/BugSplat.h>
#import <HockeySDK/HockeySDK.h>
#import "BugSplatUtilities.h"

NSString *const kHockeyIdentifierPlaceholder = @"b0cf675cb9334a3e96eda0764f95e38e"; // Just to satisfy Hockey since this is required

@interface BugSplat() <BITHockeyManagerDelegate>

/**
* Attributes represent app supplied keys and values additional to the crash report.
* Attributes will be bundled up in a BugSplatAttachment as NSData, with a filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8".
*
* NOTES:
*
*
* IMPORTANT: For iOS, if BugSplatDelegate's method `- (BugSplatAttachment *)attachmentForBugSplat:(BugSplat *)bugSplat` returns a non-nil BugSplatAttachment,
* attributes will be ignored (NOT be included in the Crash Report). This is a current limitation of the iOS BugSplat API.
*/
@property (nonatomic, nullable) NSMutableDictionary<NSString *, NSString *> *attributes;

@end

@implementation BugSplat
Expand Down Expand Up @@ -119,6 +132,28 @@ - (void)setAutoSubmitCrashReport:(BOOL)autoSubmitCrashReport

}

- (void)setValue:(nullable NSString *)value forAttribute:(NSString *)attribute
{
if (_attributes == nil && value != nil) {
_attributes = [NSMutableDictionary dictionary];
}

// clean up attribute and values
// See: https://stackoverflow.com/questions/1091945/what-characters-do-i-need-to-escape-in-xml-documents

// first remove newlines and whitespace from prefix or suffix of an attribute since these will be nodes in the XML document
NSString *cleanedUpAttribute = [attribute stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
NSString *escapedAttribute = [cleanedUpAttribute stringByEscapingXMLCharactersIgnoringCDataAndComments];

// escape xml characters in value
NSString *escapedValue = [value stringByEscapingXMLCharactersIgnoringCDataAndComments];

NSLog(@"BugSplat adding attribute [_attributes setValue%@ forKey:%@]", escapedValue, escapedAttribute);

// add to _attributes dictionary
[_attributes setValue:escapedValue forKey:escapedAttribute];
}


#if TARGET_OS_OSX

Expand Down Expand Up @@ -172,22 +207,34 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage
if ([_delegate respondsToSelector:@selector(attachmentForBugSplat:)])
{
BugSplatAttachment *attachment = [_delegate attachmentForBugSplat:self];

return [[BITHockeyAttachment alloc] initWithFilename:attachment.filename
hockeyAttachmentData:attachment.attachmentData
contentType:attachment.contentType];

if (attachment)
{
return [[BITHockeyAttachment alloc] initWithFilename:attachment.filename
hockeyAttachmentData:attachment.attachmentData
contentType:attachment.contentType];
}
}


// no delegate provided BugSplatAttachment, send attributes as attributesAttachment if present
BugSplatAttachment *attributesAttachment = [self bugSplatAttachmentWithAttributes:self.attributes];
if (attributesAttachment)
{
return [[BITHockeyAttachment alloc] initWithFilename:attributesAttachment.filename
hockeyAttachmentData:attributesAttachment.attachmentData
contentType:attributesAttachment.contentType];
}

return nil;
}

// MacOS
- (NSArray<BITHockeyAttachment *> *)attachmentsForCrashManager:(BITCrashManager *)crashManager
{
NSMutableArray *attachments = [[NSMutableArray alloc] init];

if ([_delegate respondsToSelector:@selector(attachmentsForBugSplat:)])
{
NSMutableArray *attachments = [[NSMutableArray alloc] init];

NSArray *bugsplatAttachments = [_delegate attachmentsForBugSplat:self];

for (BugSplatAttachment *attachment in bugsplatAttachments)
Expand All @@ -199,7 +246,6 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage
[attachments addObject:hockeyAttachment];
}

return [attachments copy];
}
else if ([_delegate respondsToSelector:@selector(attachmentForBugSplat:)])
{
Expand All @@ -215,7 +261,22 @@ -(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManage
hockeyAttachmentData:attachment.attachmentData
contentType:attachment.contentType];

return @[hockeyAttachment];
[attachments addObject:hockeyAttachment];
}

// include attributes as attributesAttachment if present
BugSplatAttachment *attributesAttachment = [self bugSplatAttachmentWithAttributes:self.attributes];
if (attributesAttachment)
{
BITHockeyAttachment *hockeyAttachment = [[BITHockeyAttachment alloc] initWithFilename:attributesAttachment.filename
hockeyAttachmentData:attributesAttachment.attachmentData
contentType:attributesAttachment.contentType];
[attachments addObject:hockeyAttachment];
}

if ([attachments count] > 0)
{
return [attachments copy];
}

return nil;
Expand Down Expand Up @@ -272,4 +333,50 @@ - (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager
}
}

/**
* If attributes are present, bundle them up as a BugSplatAttachment containing
* NSData created from NSString representing an XML file, filename of CrashContext.xml, MIME type of "application/xml" and encoding of "UTF-8".
*/
- (BugSplatAttachment *)bugSplatAttachmentWithAttributes:(NSDictionary *)attributes
{
if (attributes == nil || [attributes count] == 0)
{
return nil;
}

// prepare XML as stringData from attributes
// NOTE: If NSXMLDocument was available for iOS, that would be the better choice for building our XMLDocument...

NSMutableString *stringData = [NSMutableString new];
[stringData appendString:@"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"];
[stringData appendString:@"<Attributes>\n"];

// for each attribute:value pair, add <attribute>value</attribute> row to the XML stringData
for (NSString *attribute in attributes.allKeys) {
NSString *value = attributes[attribute];
[stringData appendFormat:@"<%@>", attribute];
[stringData appendString:value];
[stringData appendFormat:@"</%@>", attribute];
[stringData appendString:@"\n"];
}

[stringData appendString:@"</Attributes>\n"];

NSData *data = [stringData dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];

if (data)
{
// debug logging
NSString *debugString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (debugString)
{
NSLog(@"BugSplat adding attributes as BugSplatAttachment with contents: [%@]", debugString);
}

return [[BugSplatAttachment alloc] initWithFilename:@"CrashContext.xml" attachmentData:data contentType:@"UTF-8"];
}

return nil;
}

@end
12 changes: 12 additions & 0 deletions BugSplat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
6341B28D2BDC514C007EE8AC /* HockeySDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 63598C3C2B9BBD6600770E43 /* HockeySDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
63598C432BA0CCD500770E43 /* BugSplat.m in Sources */ = {isa = PBXBuildFile; fileRef = 63598C422BA0CCD500770E43 /* BugSplat.m */; };
63598C442BA0CCD500770E43 /* BugSplat.m in Sources */ = {isa = PBXBuildFile; fileRef = 63598C422BA0CCD500770E43 /* BugSplat.m */; };
637DF0452D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */; };
637DF0462D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */; };
637DF0482D49A50800DDD8FE /* BugSplatUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */; };
637DF0492D49A50800DDD8FE /* BugSplatUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */; };
638D38DC2BA0CEE900DC37A8 /* BugSplatDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
638D38DD2BA0CEEA00DC37A8 /* BugSplatDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
63C6E2032B92852500AED3E3 /* BugSplat.h in Headers */ = {isa = PBXBuildFile; fileRef = 63C6E1FE2B9283B000AED3E3 /* BugSplat.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -53,6 +57,8 @@
6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugSplatDelegate.h; sourceTree = "<group>"; };
63598C3C2B9BBD6600770E43 /* HockeySDK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = HockeySDK.xcframework; path = Vendor/HockeySDK.xcframework; sourceTree = "<group>"; };
63598C422BA0CCD500770E43 /* BugSplat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugSplat.m; sourceTree = "<group>"; };
637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugSplatUtilities.h; sourceTree = "<group>"; };
637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugSplatUtilities.m; sourceTree = "<group>"; };
63C6E1ED2B92831A00AED3E3 /* BugSplatMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugSplatMac.framework; sourceTree = BUILT_PRODUCTS_DIR; };
63C6E1FC2B9283B000AED3E3 /* BugSplat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BugSplat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
63C6E1FE2B9283B000AED3E3 /* BugSplat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugSplat.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -90,6 +96,8 @@
63E2337C2B9710420066C7FC /* BugSplatAttachment.h */,
63E2337A2B9710410066C7FC /* BugSplatAttachment.m */,
6344718C2B9695C500EBEBEB /* BugSplatDelegate.h */,
637DF0442D49A49C00DDD8FE /* BugSplatUtilities.h */,
637DF0472D49A50800DDD8FE /* BugSplatUtilities.m */,
63C6E1EE2B92831A00AED3E3 /* Products */,
63C6E2062B9285F400AED3E3 /* Frameworks */,
);
Expand Down Expand Up @@ -123,6 +131,7 @@
63D006FA2BBF785B00587FBF /* BugSplatMac.h in Headers */,
638D38DD2BA0CEEA00DC37A8 /* BugSplatDelegate.h in Headers */,
63E233822B9710420066C7FC /* BugSplatAttachment.h in Headers */,
637DF0452D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -133,6 +142,7 @@
63E233862B9715B50066C7FC /* BugSplat.h in Headers */,
638D38DC2BA0CEE900DC37A8 /* BugSplatDelegate.h in Headers */,
63E233842B9711AB0066C7FC /* BugSplatAttachment.h in Headers */,
637DF0462D49A49C00DDD8FE /* BugSplatUtilities.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -236,6 +246,7 @@
buildActionMask = 2147483647;
files = (
63E233802B9710420066C7FC /* BugSplatAttachment.m in Sources */,
637DF0482D49A50800DDD8FE /* BugSplatUtilities.m in Sources */,
63598C432BA0CCD500770E43 /* BugSplat.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -245,6 +256,7 @@
buildActionMask = 2147483647;
files = (
63E233852B9711B10066C7FC /* BugSplatAttachment.m in Sources */,
637DF0492D49A50800DDD8FE /* BugSplatUtilities.m in Sources */,
63598C442BA0CCD500770E43 /* BugSplat.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
7 changes: 3 additions & 4 deletions BugSplatDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSString *)applicationKeyForBugSplat:(BugSplat *)bugSplat signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason API_AVAILABLE(macosx(10.13));

/** Return a collection of BugsplatAttachment objects providing an NSData object the crash report
being processed should contain
/** Return a collection of BugsplatAttachment objects providing an NSData object the crash report being processed should contain
Example implementation:
Expand All @@ -87,8 +86,8 @@ NS_ASSUME_NONNULL_BEGIN

// MARK: - BugSplatDelegate (iOS)

/** Return a collection of BugSplatAttachment objects providing an NSData object the crash report
being processed should contain
/** Return a BugSplatAttachment object providing an NSData object the crash report being processed should contain
NOTE: If this method returns a non-nil BugSplatAttachment, any attributes added via setAttribute:value: to BugSplat will NOT be included in the Crash Report.
Example implementation:
Expand Down
53 changes: 53 additions & 0 deletions BugSplatUtilities.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// BugSplatUtilities.h
//
// Copyright © BugSplat, LLC. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSArray (XMLArrayUtility)

/**
* Given an array of tokenPairRange values, remove any tokenPairRange values that are
* either fully contained within another range, or overlapping (partially contained) within another range.
* return: validTokenPairRange in ascending range order by location.
*/
+ (NSArray<NSValue *> *)validTokenPairRanges:(NSArray<NSValue *> *)tokenPairRanges;

@end


@interface NSString (XMLStringUtility)

/**
* Utility method to clean up special characters found in attribute or value strings
*
* NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary)
* This method considers CDATA, and Comments, but currently omits Processing Instructions when escaping is done on the receiver.
*/
- (NSString *)stringByEscapingXMLCharactersIgnoringCDataAndComments;


/**
* Utility method to clean up special characters found in attribute or value strings
*
* NOTE: API based on macOS 10.3+ only API CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary)
* This method does not consider CDATA, Comments, nor Processing Instructions when escaping is done on the receiver. See other methods to first identify these escape-exclusion ranges.
*/
- (NSString *)stringByEscapingSpecialXMLCharacters;

/**
* Given a start token and end token pair, search receiver, returning array of NSValue objects containing NSRange of each pair found
* Return Array will be empty if no pairs are found.
* Error will be nil if no parsing errors occur.
*/
- (NSArray<NSValue *> *)tokenPairRangesForStartToken:(NSString *)startToken endToken:(NSString *)endToken error:(NSError **)error;

/**
* Given a string, and an ascendingExclusionRanges sorted array (based on this receiver), escape the 5 special XML characters
* within the receiver, taking care not to escape any characters within the exclusionRanges.
*/
- (NSString *)stringByXMLEscapingWithExclusionRanges:(NSArray<NSValue *> *)ascendingExclusionRanges;

@end
Loading

0 comments on commit ae201ec

Please sign in to comment.