From 1a18683901844a2970ccfb633e4ebae565361817 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 20 Dec 2022 06:20:03 -0300 Subject: [PATCH] feat: Update view hierarchy attachment format to Json (#2491) Change view hierarchy format to conform to RFC 33 Closes GH-2467 --- CHANGELOG.md | 1 + Sentry.xcodeproj/project.pbxproj | 22 +++ Sources/Sentry/PrivateSentrySDKOnly.m | 6 + Sources/Sentry/Public/Sentry.h | 1 + .../Sentry/Public/SentryEnvelopeItemHeader.h | 25 +++ Sources/Sentry/Public/SentrySerializable.h | 3 +- Sources/Sentry/SentryAttachment.m | 54 ++++++- Sources/Sentry/SentryCrashReportSink.m | 4 +- Sources/Sentry/SentryEnvelope.m | 40 ++--- .../Sentry/SentryEnvelopeAttachmentHeader.m | 34 ++++ Sources/Sentry/SentryEnvelopeItemHeader.m | 47 ++++++ Sources/Sentry/SentryScope.m | 15 +- Sources/Sentry/SentrySerialization.m | 38 ++--- Sources/Sentry/SentryViewHierarchy.m | 146 +++++++++++++++--- .../Sentry/SentryViewHierarchyIntegration.m | 23 ++- .../HybridPublic/PrivateSentrySDKOnly.h | 3 +- .../include/HybridPublic/SentryEnvelope.h | 24 +-- .../Sentry/include/SentryAttachment+Private.h | 59 +++++++ .../Sentry/include/SentryEnvelope+Private.h | 1 + .../include/SentryEnvelopeAttachmentHeader.h | 18 +++ Sources/Sentry/include/SentryScope+Private.h | 2 + Sources/Sentry/include/SentryViewHierarchy.h | 4 +- .../SentryViewHierarchyIntegrationTests.swift | 33 ++-- .../ViewHierarchy/TestSentryViewHierarchy.h | 13 ++ .../TestSentryViewHierarchy.swift | 19 ++- .../Protocol/SentryAttachment+Equality.m | 4 + .../Protocol/SentryEnvelopeTests.swift | 64 +++++++- Tests/SentryTests/SentryClientTests.swift | 14 ++ .../SentryTests/SentryTests-Bridging-Header.h | 4 +- .../SentryViewHierarchyTests.swift | 139 ++++++++++++++++- 30 files changed, 705 insertions(+), 155 deletions(-) create mode 100644 Sources/Sentry/Public/SentryEnvelopeItemHeader.h create mode 100644 Sources/Sentry/SentryEnvelopeAttachmentHeader.m create mode 100644 Sources/Sentry/SentryEnvelopeItemHeader.m create mode 100644 Sources/Sentry/include/SentryAttachment+Private.h create mode 100644 Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h create mode 100644 Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 35fd78a13b..dceda07572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This version adds a dependency on Swift. ### Features - Properly demangle Swift class name (#2162) +- Change view hierarchy attachment format to JSON (#2491) - SwiftUI performance tracking (#2271) - Enable [File I/O Tracking](https://docs.sentry.io/platforms/apple/performance/instrumentation/automatic-instrumentation/#file-io-tracking) by default (#2497) - [User Interaction Tracing](https://docs.sentry.io/platforms/apple/performance/instrumentation/automatic-instrumentation/#user-interaction-tracing) is stable and enabled by default(#2503) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d61a144631..fc683b972f 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -708,6 +708,7 @@ D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */; }; D867063E27C3BC2400048851 /* SentryCoreDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063B27C3BC2400048851 /* SentryCoreDataSwizzling.h */; }; D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; }; + D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; }; D86F419827C8FEFA00490520 /* SentryCoreDataMiddleware+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataMiddleware+Extension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; @@ -733,6 +734,10 @@ D8BD2E6829361A0F00D96C6A /* PrivatesHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8BD2E67293619F600D96C6A /* PrivatesHeader.h */; settings = {ATTRIBUTES = (Private, ); }; }; D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9928000E23007E326E /* SentryUIApplication.h */; }; D8C67E9C28000E24007E326E /* SentryScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9A28000E23007E326E /* SentryScreenshot.h */; }; + D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; + D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; + D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB741A2947286500A5F964 /* SentryEnvelopeItemHeader.m */; }; D8CB742B294B1DD000A5F964 /* SentryUIApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CB742A294B1DD000A5F964 /* SentryUIApplicationTests.swift */; }; D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB742D294B294B00A5F964 /* MockUIScene.m */; }; D8CE69BC277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */; }; @@ -1546,6 +1551,8 @@ D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTrackingIntegration.h; path = include/SentryCoreDataTrackingIntegration.h; sourceTree = ""; }; D867063B27C3BC2400048851 /* SentryCoreDataSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataSwizzling.h; path = include/SentryCoreDataSwizzling.h; sourceTree = ""; }; D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTracker.h; path = include/SentryCoreDataTracker.h; sourceTree = ""; }; + D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = ""; }; + D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = ""; }; D86F419727C8FEFA00490520 /* SentryCoreDataMiddleware+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryCoreDataMiddleware+Extension.swift"; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; @@ -1574,6 +1581,10 @@ D8CB742A294B1DD000A5F964 /* SentryUIApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIApplicationTests.swift; sourceTree = ""; }; D8CB742C294B294B00A5F964 /* MockUIScene.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockUIScene.h; sourceTree = ""; }; D8CB742D294B294B00A5F964 /* MockUIScene.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MockUIScene.m; sourceTree = ""; }; + D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; + D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; + D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; + D8CB741A2947286500A5F964 /* SentryEnvelopeItemHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeItemHeader.m; sourceTree = ""; }; D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegrationObjCTests.m; sourceTree = ""; }; D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryPredicateDescriptor.h; path = include/SentryPredicateDescriptor.h; sourceTree = ""; }; @@ -1644,6 +1655,7 @@ children = ( 0A9BF4EA28A127120068D266 /* SentryViewHierarchyIntegrationTests.swift */, 0A9BF4E628A123270068D266 /* TestSentryViewHierarchy.swift */, + D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */, ); path = ViewHierarchy; sourceTree = ""; @@ -1694,6 +1706,10 @@ 15E0A8E0240C41CE00F044E3 /* SentryEnvelope.h */, 7B5CAF7027F5953400ED0DB6 /* SentryEnvelope+Private.h */, 15E0A8E4240C457D00F044E3 /* SentryEnvelope.m */, + D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */, + D8CB741A2947286500A5F964 /* SentryEnvelopeItemHeader.m */, + D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */, + D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */, 7BC852382458830A005A70F0 /* SentryEnvelopeItemType.h */, 6304360F1EC0600A00C4D3FA /* SentrySerializable.h */, 639FCF961EBC7B9700778193 /* SentryEvent.h */, @@ -1732,6 +1748,7 @@ 7BB654FA253DC14A00887E87 /* SentryUserFeedback.h */, 7BB65500253DC1B500887E87 /* SentryUserFeedback.m */, 7B4E375425822C4500059C93 /* SentryAttachment.h */, + D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */, 7B4E375E258231FC00059C93 /* SentryAttachment.m */, 7BD4BD4227EB29BA0071F4FF /* SentryClientReport.h */, 7BD4BD4427EB29F50071F4FF /* SentryClientReport.m */, @@ -3097,6 +3114,7 @@ 03F84D2227DD414C008FE43F /* SentryStackFrame.hpp in Headers */, 8ECC673E25C23996000E2BF6 /* SentrySpanContext.h in Headers */, 8ECC674025C23996000E2BF6 /* SentryTransactionContext.h in Headers */, + D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */, 63FE71BA20DA4C1100CDBAE8 /* SentryCrashInstallation+Private.h in Headers */, 63FE71AE20DA4C1100CDBAE8 /* SentryCrashInstallation.h in Headers */, 63FE70F120DA4C1000CDBAE8 /* SentryCrashMonitorType.h in Headers */, @@ -3122,6 +3140,7 @@ D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */, 7B6438AA26A70F24000D0F65 /* UIViewController+Sentry.h in Headers */, 639FCFAC1EBC811400778193 /* SentryUser.h in Headers */, + D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, 7D7F0A5F23DF3D2C00A4629C /* SentryGlobalEventProcessor.h in Headers */, 7B82D54524E2A05500EE670F /* SentryId.h in Headers */, D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */, @@ -3165,6 +3184,7 @@ 7B18DE4028D9F748004845C6 /* SentryNSNotificationCenterWrapper.h in Headers */, 03F84D1E27DD414C008FE43F /* SentryBacktrace.hpp in Headers */, 63AA76991EB9C1C200D153DE /* SentryDefines.h in Headers */, + D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */, 0A4EDEA928D3461B00FA67CB /* SentryPerformanceTracker+Private.h in Headers */, 7B2A70DB27D607CF008B0D15 /* SentryThreadWrapper.h in Headers */, 8EAE980B261E9F530073B6B3 /* SentryPerformanceTracker.h in Headers */, @@ -3590,6 +3610,8 @@ 7BE3C7692445C1A800A38442 /* SentryCurrentDate.m in Sources */, 7BCFA71627D0BB50008C662C /* SentryANRTracker.m in Sources */, 63EED6C02237923600E02400 /* SentryOptions.m in Sources */, + D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */, + D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, 63AA769E1EB9C57A00D153DE /* SentryError.m in Sources */, 7B8713B026415B22006D6004 /* SentryAppStartTrackingIntegration.m in Sources */, diff --git a/Sources/Sentry/PrivateSentrySDKOnly.m b/Sources/Sentry/PrivateSentrySDKOnly.m index 040038fa55..c60549fe3d 100644 --- a/Sources/Sentry/PrivateSentrySDKOnly.m +++ b/Sources/Sentry/PrivateSentrySDKOnly.m @@ -6,6 +6,7 @@ #import "SentryMeta.h" #import "SentrySDK+Private.h" #import "SentrySerialization.h" +#import "SentryViewHierarchy.h" #import #import #import @@ -126,6 +127,11 @@ + (SentryScreenFrames *)currentScreenFrames return [SentryDependencyContainer.sharedInstance.screenshot takeScreenshots]; } ++ (NSData *)captureViewHierarchy +{ + return [SentryDependencyContainer.sharedInstance.viewHierarchy fetchViewHierarchy]; +} + #endif @end diff --git a/Sources/Sentry/Public/Sentry.h b/Sources/Sentry/Public/Sentry.h index a4f993ae7f..df484339c6 100644 --- a/Sources/Sentry/Public/Sentry.h +++ b/Sources/Sentry/Public/Sentry.h @@ -14,6 +14,7 @@ FOUNDATION_EXPORT const unsigned char SentryVersionString[]; #import "SentryDebugMeta.h" #import "SentryDefines.h" #import "SentryDsn.h" +#import "SentryEnvelopeItemHeader.h" #import "SentryError.h" #import "SentryEvent.h" #import "SentryException.h" diff --git a/Sources/Sentry/Public/SentryEnvelopeItemHeader.h b/Sources/Sentry/Public/SentryEnvelopeItemHeader.h new file mode 100644 index 0000000000..493c4d6831 --- /dev/null +++ b/Sources/Sentry/Public/SentryEnvelopeItemHeader.h @@ -0,0 +1,25 @@ +#import "SentrySerializable.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryEnvelopeItemHeader : NSObject +SENTRY_NO_INIT + +- (instancetype)initWithType:(NSString *)type length:(NSUInteger)length NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithType:(NSString *)type + length:(NSUInteger)length + filenname:(NSString *)filename + contentType:(NSString *)contentType; + +/** + * The type of the envelope item. + */ +@property (nonatomic, readonly, copy) NSString *type; +@property (nonatomic, readonly) NSUInteger length; +@property (nonatomic, readonly, copy, nullable) NSString *filename; +@property (nonatomic, readonly, copy, nullable) NSString *contentType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/Public/SentrySerializable.h b/Sources/Sentry/Public/SentrySerializable.h index 4e092d9a1f..16d5fcf1e7 100644 --- a/Sources/Sentry/Public/SentrySerializable.h +++ b/Sources/Sentry/Public/SentrySerializable.h @@ -1,6 +1,5 @@ -#import - #import "SentryDefines.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/SentryAttachment.m b/Sources/Sentry/SentryAttachment.m index 3e4a96f9c0..c0a34b386c 100644 --- a/Sources/Sentry/SentryAttachment.m +++ b/Sources/Sentry/SentryAttachment.m @@ -1,4 +1,4 @@ -#import "SentryAttachment.h" +#import "SentryAttachment+Private.h" #import NS_ASSUME_NONNULL_BEGIN @@ -7,18 +7,33 @@ @implementation SentryAttachment - (instancetype)initWithData:(NSData *)data filename:(NSString *)filename { - return [self initWithData:data filename:filename contentType:nil]; + return [self initWithData:data + filename:filename + contentType:nil + attachmentType:kSentryAttachmentTypeEventAttachment]; } - (instancetype)initWithData:(NSData *)data filename:(NSString *)filename contentType:(nullable NSString *)contentType { + return [self initWithData:data + filename:filename + contentType:contentType + attachmentType:kSentryAttachmentTypeEventAttachment]; +} + +- (instancetype)initWithData:(NSData *)data + filename:(NSString *)filename + contentType:(nullable NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType +{ if (self = [super init]) { _data = data; _filename = filename; _contentType = contentType; + _attachmentType = attachmentType; } return self; } @@ -36,15 +51,50 @@ - (instancetype)initWithPath:(NSString *)path filename:(NSString *)filename - (instancetype)initWithPath:(NSString *)path filename:(NSString *)filename contentType:(nullable NSString *)contentType +{ + return [self initWithPath:path + filename:filename + contentType:contentType + attachmentType:kSentryAttachmentTypeEventAttachment]; +} + +- (instancetype)initWithPath:(NSString *)path + filename:(NSString *)filename + contentType:(nullable NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType { if (self = [super init]) { _path = path; _filename = filename; _contentType = contentType; + _attachmentType = attachmentType; } return self; } @end +NSString *const kSentryAttachmentTypeNameEventAttachment = @"event.attachment"; +NSString *const kSentryAttachmentTypeNameViewHierarchy = @"event.view_hierarchy"; + +NSString * +nameForSentryAttachmentType(SentryAttachmentType attachmentType) +{ + switch (attachmentType) { + case kSentryAttachmentTypeViewHierarchy: + return kSentryAttachmentTypeNameViewHierarchy; + default: + return kSentryAttachmentTypeNameEventAttachment; + } +} + +SentryAttachmentType +typeForSentryAttachmentName(NSString *name) +{ + if ([name isEqualToString:kSentryAttachmentTypeNameViewHierarchy]) { + return kSentryAttachmentTypeViewHierarchy; + } + return kSentryAttachmentTypeEventAttachment; +} + NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryCrashReportSink.m b/Sources/Sentry/SentryCrashReportSink.m index eb7e137647..c40e350bce 100644 --- a/Sources/Sentry/SentryCrashReportSink.m +++ b/Sources/Sentry/SentryCrashReportSink.m @@ -13,7 +13,7 @@ #import "SentryLog.h" #import "SentrySDK+Private.h" #import "SentrySDK.h" -#import "SentryScope.h" +#import "SentryScope+Private.h" #import "SentryThread.h" static const NSTimeInterval SENTRY_APP_START_CRASH_DURATION_THRESHOLD = 2.0; @@ -94,7 +94,7 @@ - (void)handleConvertedEvent:(SentryEvent *)event if (report[SENTRYCRASH_REPORT_ATTACHMENTS_ITEM]) { for (NSString *ssPath in report[SENTRYCRASH_REPORT_ATTACHMENTS_ITEM]) { - [scope addAttachment:[[SentryAttachment alloc] initWithPath:ssPath]]; + [scope addCrashReportAttachmentInPath:ssPath]; } } diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index 4389ea8e57..ba290b064f 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -1,7 +1,9 @@ -#import "SentryEnvelope.h" #import "SentryAttachment.h" #import "SentryBreadcrumb.h" #import "SentryClientReport.h" +#import "SentryEnvelope+Private.h" +#import "SentryEnvelopeAttachmentHeader.h" +#import "SentryEnvelopeItemHeader.h" #import "SentryEnvelopeItemType.h" #import "SentryEvent.h" #import "SentryLog.h" @@ -48,31 +50,6 @@ - (instancetype)initWithId:(nullable SentryId *)eventId @end -@implementation SentryEnvelopeItemHeader - -- (instancetype)initWithType:(NSString *)type length:(NSUInteger)length -{ - if (self = [super init]) { - _type = type; - _length = length; - } - return self; -} - -- (instancetype)initWithType:(NSString *)type - length:(NSUInteger)length - filenname:(NSString *)filename - contentType:(NSString *)contentType -{ - if (self = [self initWithType:type length:length]) { - _filename = filename; - _contentType = contentType; - } - return self; -} - -@end - @implementation SentryEnvelopeItem - (instancetype)initWithHeader:(SentryEnvelopeItemHeader *)header data:(NSData *)data @@ -210,16 +187,17 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment data = [[NSFileManager defaultManager] contentsAtPath:attachment.path]; } - if (nil == data) { + if (data == nil) { SENTRY_LOG_ERROR(@"Couldn't init Attachment."); return nil; } SentryEnvelopeItemHeader *itemHeader = - [[SentryEnvelopeItemHeader alloc] initWithType:SentryEnvelopeItemTypeAttachment - length:data.length - filenname:attachment.filename - contentType:attachment.contentType]; + [[SentryEnvelopeAttachmentHeader alloc] initWithType:SentryEnvelopeItemTypeAttachment + length:data.length + filename:attachment.filename + contentType:attachment.contentType + attachmentType:attachment.attachmentType]; return [self initWithHeader:itemHeader data:data]; } diff --git a/Sources/Sentry/SentryEnvelopeAttachmentHeader.m b/Sources/Sentry/SentryEnvelopeAttachmentHeader.m new file mode 100644 index 0000000000..309354107c --- /dev/null +++ b/Sources/Sentry/SentryEnvelopeAttachmentHeader.m @@ -0,0 +1,34 @@ +#import "SentryEnvelopeAttachmentHeader.h" +#import "SentryEnvelope+Private.h" + +@implementation SentryEnvelopeAttachmentHeader + +- (instancetype)initWithType:(NSString *)type length:(NSUInteger)length +{ + if (self = [super initWithType:type length:length]) { + _attachmentType = kSentryAttachmentTypeEventAttachment; + } + return self; +} + +- (instancetype)initWithType:(NSString *)type + length:(NSUInteger)length + filename:(NSString *)filename + contentType:(NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType +{ + + if (self = [self initWithType:type length:length filenname:filename contentType:contentType]) { + _attachmentType = attachmentType; + } + return self; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *result = [[super serialize] mutableCopy]; + [result setObject:nameForSentryAttachmentType(self.attachmentType) forKey:@"attachment_type"]; + return result; +} + +@end diff --git a/Sources/Sentry/SentryEnvelopeItemHeader.m b/Sources/Sentry/SentryEnvelopeItemHeader.m new file mode 100644 index 0000000000..153c32028e --- /dev/null +++ b/Sources/Sentry/SentryEnvelopeItemHeader.m @@ -0,0 +1,47 @@ +#import "SentryEnvelopeItemHeader.h" + +@implementation SentryEnvelopeItemHeader + +- (instancetype)initWithType:(NSString *)type length:(NSUInteger)length +{ + if (self = [super init]) { + _type = type; + _length = length; + } + return self; +} + +- (instancetype)initWithType:(NSString *)type + length:(NSUInteger)length + filenname:(NSString *)filename + contentType:(NSString *)contentType +{ + if (self = [self initWithType:type length:length]) { + _filename = filename; + _contentType = contentType; + } + return self; +} + +- (NSDictionary *)serialize +{ + + NSMutableDictionary *target = [[NSMutableDictionary alloc] init]; + if (self.type) { + [target setValue:self.type forKey:@"type"]; + } + + if (self.filename) { + [target setValue:self.filename forKey:@"filename"]; + } + + if (self.contentType) { + [target setValue:self.contentType forKey:@"content_type"]; + } + + [target setValue:[NSNumber numberWithUnsignedInteger:self.length] forKey:@"length"]; + + return target; +} + +@end diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 04cb28d382..d2c25b1234 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -1,6 +1,6 @@ #import "SentryScope.h" #import "NSMutableDictionary+Sentry.h" -#import "SentryAttachment.h" +#import "SentryAttachment+Private.h" #import "SentryBreadcrumb.h" #import "SentryEnvelopeItemType.h" #import "SentryEvent.h" @@ -396,6 +396,19 @@ - (void)addAttachment:(SentryAttachment *)attachment } } +- (void)addCrashReportAttachmentInPath:(NSString *)filePath +{ + if ([filePath.lastPathComponent isEqualToString:@"view-hierarchy.json"]) { + [self addAttachment:[[SentryAttachment alloc] + initWithPath:filePath + filename:@"view-hierarchy.json" + contentType:@"application/json" + attachmentType:kSentryAttachmentTypeViewHierarchy]]; + } else { + [self addAttachment:[[SentryAttachment alloc] initWithPath:filePath]]; + } +} + - (void)clearAttachments { @synchronized(_attachmentArray) { diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m index 2404f76696..2a423b53c1 100644 --- a/Sources/Sentry/SentrySerialization.m +++ b/Sources/Sentry/SentrySerialization.m @@ -1,6 +1,7 @@ #import "SentrySerialization.h" #import "SentryAppState.h" -#import "SentryEnvelope.h" +#import "SentryEnvelope+Private.h" +#import "SentryEnvelopeAttachmentHeader.h" #import "SentryEnvelopeItemType.h" #import "SentryError.h" #import "SentryId.h" @@ -81,26 +82,8 @@ + (NSData *_Nullable)dataWithEnvelope:(SentryEnvelope *)envelope for (int i = 0; i < envelope.items.count; ++i) { [envelopeData appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; - NSMutableDictionary *serializedItemHeaderData = [NSMutableDictionary new]; - if (nil != envelope.items[i].header) { - if (nil != envelope.items[i].header.type) { - [serializedItemHeaderData setValue:envelope.items[i].header.type forKey:@"type"]; - } - - NSString *filename = envelope.items[i].header.filename; - if (nil != filename) { - [serializedItemHeaderData setValue:filename forKey:@"filename"]; - } + NSDictionary *serializedItemHeaderData = [envelope.items[i].header serialize]; - NSString *contentType = envelope.items[i].header.contentType; - if (nil != contentType) { - [serializedItemHeaderData setValue:contentType forKey:@"content_type"]; - } - - [serializedItemHeaderData - setValue:[NSNumber numberWithUnsignedInteger:envelope.items[i].header.length] - forKey:@"length"]; - } NSData *itemHeader = [SentrySerialization dataWithJSONObject:serializedItemHeaderData error:error]; if (nil == itemHeader) { @@ -280,15 +263,18 @@ + (SentryEnvelope *_Nullable)envelopeWithData:(NSData *)data break; } - NSString *_Nullable filename = [headerDictionary valueForKey:@"filename"]; - NSString *_Nullable contentType = [headerDictionary valueForKey:@"content_type"]; + NSString *filename = [headerDictionary valueForKey:@"filename"]; + NSString *contentType = [headerDictionary valueForKey:@"content_type"]; + NSString *attachmentType = [headerDictionary valueForKey:@"attachment_type"]; SentryEnvelopeItemHeader *itemHeader; if (nil != filename) { - itemHeader = [[SentryEnvelopeItemHeader alloc] initWithType:type - length:bodyLength - filenname:filename - contentType:contentType]; + itemHeader = [[SentryEnvelopeAttachmentHeader alloc] + initWithType:type + length:bodyLength + filename:filename + contentType:contentType + attachmentType:typeForSentryAttachmentName(attachmentType)]; } else { itemHeader = [[SentryEnvelopeItemHeader alloc] initWithType:type length:bodyLength]; } diff --git a/Sources/Sentry/SentryViewHierarchy.m b/Sources/Sentry/SentryViewHierarchy.m index 05e7369240..b846c7b6a5 100644 --- a/Sources/Sentry/SentryViewHierarchy.m +++ b/Sources/Sentry/SentryViewHierarchy.m @@ -1,48 +1,150 @@ #import "SentryViewHierarchy.h" +#import "SentryCrashFileUtils.h" +#import "SentryCrashJSONCodec.h" #import "SentryDependencyContainer.h" +#import "SentryLog.h" #import "SentryUIApplication.h" #import "UIView+Sentry.h" +@import SentryPrivate; + #if SENTRY_HAS_UIKIT # import +static int +writeJSONDataToFile(const char *const data, const int length, void *const userData) +{ + const int fd = *((int *)userData); + const bool success = sentrycrashfu_writeBytesToFD(fd, data, length); + return success ? SentryCrashJSON_OK : SentryCrashJSON_ERROR_CANNOT_ADD_DATA; +} + +static int +writeJSONDataToMemory(const char *const data, const int length, void *const userData) +{ + NSMutableData *memory = ((__bridge NSMutableData *)userData); + [memory appendBytes:data length:length]; + return SentryCrashJSON_OK; +} + @implementation SentryViewHierarchy -- (NSArray *)fetchViewHierarchy +- (BOOL)saveViewHierarchy:(NSString *)filePath { - return [self fetchViewHierarchyPreventMoveToMainThread:NO]; + NSArray *windows = [SentryDependencyContainer.sharedInstance.application windows]; + + const char *path = [filePath UTF8String]; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + SENTRY_LOG_DEBUG(@"Could not open file %s for writing: %s", path, strerror(errno)); + return false; + } + + BOOL result = [self processViewHierarchy:windows addFunction:writeJSONDataToFile userData:&fd]; + + close(fd); + return result; } -- (NSArray *)fetchViewHierarchyPreventMoveToMainThread:(BOOL)preventMoveToMainThread +- (NSData *)fetchViewHierarchy { NSArray *windows = [SentryDependencyContainer.sharedInstance.application windows]; - NSMutableArray *result = [NSMutableArray arrayWithCapacity:[windows count]]; + __block NSMutableData *result = [[NSMutableData alloc] init]; - [windows enumerateObjectsUsingBlock:^(UIWindow *window, NSUInteger idx, BOOL *stop) { - // In the case of a crash we can't dispatch work to be executed anymore, - // so we'll run this on the wrong thread. - if ([NSThread isMainThread] || preventMoveToMainThread) { - [result addObject:[window sentry_recursiveViewHierarchyDescription]]; - } else { - dispatch_sync(dispatch_get_main_queue(), - ^{ [result addObject:[window sentry_recursiveViewHierarchyDescription]]; }); + void (^save)(void) = ^{ + if (![self processViewHierarchy:windows + addFunction:writeJSONDataToMemory + userData:(__bridge void *)(result)]) { + + result = nil; } - }]; + }; + + if ([NSThread isMainThread]) { + save(); + } else { + dispatch_sync(dispatch_get_main_queue(), save); + } return result; } -- (void)saveViewHierarchy:(NSString *)path +# define tryJson(code) \ + if ((result = (code)) != SentryCrashJSON_OK) \ + return result; + +- (BOOL)processViewHierarchy:(NSArray *)windows + addFunction:(SentryCrashJSONAddDataFunc)addJSONDataFunc + userData:(void *const)userData { - [[self fetchViewHierarchyPreventMoveToMainThread:YES] - enumerateObjectsUsingBlock:^(NSString *description, NSUInteger idx, BOOL *stop) { - NSString *fileName = - [NSString stringWithFormat:@"view-hierarchy-%lu.txt", (unsigned long)idx]; - NSString *filePath = [path stringByAppendingPathComponent:fileName]; - NSData *data = [description dataUsingEncoding:NSUTF8StringEncoding]; - [data writeToFile:filePath atomically:YES]; - }]; + + __block SentryCrashJSONEncodeContext JSONContext; + sentrycrashjson_beginEncode(&JSONContext, false, addJSONDataFunc, userData); + + int (^serializeJson)(void) = ^int() { + int result; + tryJson(sentrycrashjson_beginObject(&JSONContext, NULL)); + tryJson(sentrycrashjson_addStringElement( + &JSONContext, "rendering_system", "UIKIT", SentryCrashJSON_SIZE_AUTOMATIC)); + tryJson(sentrycrashjson_beginArray(&JSONContext, "windows")); + + for (UIView *window in windows) { + tryJson([self viewHierarchyFromView:window intoContext:&JSONContext]); + } + + tryJson(sentrycrashjson_endContainer(&JSONContext)); + + result = sentrycrashjson_endEncode(&JSONContext); + return result; + }; + + int result = serializeJson(); + if (result != SentryCrashJSON_OK) { + SENTRY_LOG_DEBUG( + @"Could not create view hierarchy json: %s", sentrycrashjson_stringForError(result)); + return false; + } + return true; +} + +- (int)viewHierarchyFromView:(UIView *)view intoContext:(SentryCrashJSONEncodeContext *)context +{ + int result = 0; + tryJson(sentrycrashjson_beginObject(context, NULL)); + const char *viewClassName = [[SwiftDescriptor getObjectClassName:view] UTF8String]; + tryJson(sentrycrashjson_addStringElement( + context, "type", viewClassName, SentryCrashJSON_SIZE_AUTOMATIC)); + + if (view.accessibilityIdentifier && view.accessibilityIdentifier.length != 0) { + tryJson(sentrycrashjson_addStringElement(context, "identifier", + view.accessibilityIdentifier.UTF8String, SentryCrashJSON_SIZE_AUTOMATIC)); + } + + tryJson(sentrycrashjson_addFloatingPointElement(context, "width", view.frame.size.width)); + tryJson(sentrycrashjson_addFloatingPointElement(context, "height", view.frame.size.height)); + tryJson(sentrycrashjson_addFloatingPointElement(context, "x", view.frame.origin.x)); + tryJson(sentrycrashjson_addFloatingPointElement(context, "y", view.frame.origin.y)); + tryJson(sentrycrashjson_addFloatingPointElement(context, "alpha", view.alpha)); + tryJson(sentrycrashjson_addBooleanElement(context, "visible", !view.hidden)); + + if ([view.nextResponder isKindOfClass:[UIViewController self]]) { + UIViewController *vc = (UIViewController *)view.nextResponder; + if (vc.view == view) { + const char *viewControllerClassName = + [[SwiftDescriptor getObjectClassName:vc] UTF8String]; + tryJson(sentrycrashjson_addStringElement(context, "view_controller", + viewControllerClassName, SentryCrashJSON_SIZE_AUTOMATIC)); + } + } + + tryJson(sentrycrashjson_beginArray(context, "children")); + for (UIView *child in view.subviews) { + tryJson([self viewHierarchyFromView:child intoContext:context]); + } + tryJson(sentrycrashjson_endContainer(context)); + tryJson(sentrycrashjson_endContainer(context)); + return result; } @end diff --git a/Sources/Sentry/SentryViewHierarchyIntegration.m b/Sources/Sentry/SentryViewHierarchyIntegration.m index 2915253687..39072a6834 100644 --- a/Sources/Sentry/SentryViewHierarchyIntegration.m +++ b/Sources/Sentry/SentryViewHierarchyIntegration.m @@ -1,5 +1,5 @@ #import "SentryViewHierarchyIntegration.h" -#import "SentryAttachment.h" +#import "SentryAttachment+Private.h" #import "SentryCrashC.h" #import "SentryDependencyContainer.h" #import "SentryEvent+Private.h" @@ -54,20 +54,17 @@ - (void)uninstall return attachments; } - NSArray *decriptions = - [SentryDependencyContainer.sharedInstance.viewHierarchy fetchViewHierarchy]; - NSMutableArray *result = - [NSMutableArray arrayWithCapacity:attachments.count + decriptions.count]; - [result addObjectsFromArray:attachments]; + NSMutableArray *result = [NSMutableArray arrayWithArray:attachments]; - [decriptions enumerateObjectsUsingBlock:^(NSString *decription, NSUInteger idx, BOOL *stop) { - SentryAttachment *attachment = [[SentryAttachment alloc] - initWithData:[decription dataUsingEncoding:NSUTF8StringEncoding] - filename:[NSString stringWithFormat:@"view-hierarchy-%lu.txt", (unsigned long)idx] - contentType:@"text/plain"]; - [result addObject:attachment]; - }]; + NSData *viewHierarchy = + [SentryDependencyContainer.sharedInstance.viewHierarchy fetchViewHierarchy]; + SentryAttachment *attachment = + [[SentryAttachment alloc] initWithData:viewHierarchy + filename:@"view-hierarchy.json" + contentType:@"application/json" + attachmentType:kSentryAttachmentTypeViewHierarchy]; + [result addObject:attachment]; return result; } diff --git a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h index 6931448148..c09e5d000a 100644 --- a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h @@ -1,6 +1,5 @@ #import "PrivatesHeader.h" #import "SentryAppStartMeasurement.h" -#import "SentryEnvelope.h" #import "SentryEnvelopeItemType.h" #import "SentryScreenFrames.h" @@ -94,6 +93,8 @@ typedef void (^SentryOnAppStartMeasurementAvailable)( @property (class, nonatomic, assign, readonly) SentryScreenFrames *currentScreenFrames; + (NSArray *)captureScreenshots; + ++ (NSData *)captureViewHierarchy; #endif @end diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h index 6f7aef795d..c304cf0b69 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h @@ -1,8 +1,8 @@ #import "PrivatesHeader.h" -#import +#import "SentryEnvelopeItemHeader.h" @class SentryEvent, SentrySession, SentrySdkInfo, SentryId, SentryUserFeedback, SentryAttachment, - SentryTransaction, SentryTraceContext, SentryClientReport; + SentryTransaction, SentryTraceContext, SentryClientReport, SentryEnvelopeItemHeader; NS_ASSUME_NONNULL_BEGIN @@ -57,26 +57,6 @@ SENTRY_NO_INIT @end -@interface SentryEnvelopeItemHeader : NSObject -SENTRY_NO_INIT - -- (instancetype)initWithType:(NSString *)type length:(NSUInteger)length NS_DESIGNATED_INITIALIZER; - -- (instancetype)initWithType:(NSString *)type - length:(NSUInteger)length - filenname:(NSString *)filename - contentType:(NSString *)contentType; - -/** - * The type of the envelope item. - */ -@property (nonatomic, readonly, copy) NSString *type; -@property (nonatomic, readonly) NSUInteger length; -@property (nonatomic, readonly, copy) NSString *_Nullable filename; -@property (nonatomic, readonly, copy) NSString *_Nullable contentType; - -@end - @interface SentryEnvelopeItem : NSObject SENTRY_NO_INIT diff --git a/Sources/Sentry/include/SentryAttachment+Private.h b/Sources/Sentry/include/SentryAttachment+Private.h new file mode 100644 index 0000000000..967b62d02e --- /dev/null +++ b/Sources/Sentry/include/SentryAttachment+Private.h @@ -0,0 +1,59 @@ +#import "SentryAttachment.h" +#import "SentryDefines.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString *const kSentryAttachmentTypeNameEventAttachment; +FOUNDATION_EXPORT NSString *const kSentryAttachmentTypeNameViewHierarchy; + +/** + * Attachment Type + */ +typedef NS_ENUM(NSInteger, SentryAttachmentType) { + kSentryAttachmentTypeEventAttachment, + kSentryAttachmentTypeViewHierarchy +}; + +NSString *nameForSentryAttachmentType(SentryAttachmentType attachmentType); + +SentryAttachmentType typeForSentryAttachmentName(NSString *name); + +@interface +SentryAttachment () +SENTRY_NO_INIT + +/** + * Initializes an attachment with data. + * + * @param data The data for the attachment. + * @param filename The name of the attachment to display in Sentry. + * @param contentType The content type of the attachment. Default is "application/octet-stream". + * @param attachmentType The type of the attachment. Default is "EventAttachment". + */ +- (instancetype)initWithData:(NSData *)data + filename:(NSString *)filename + contentType:(nullable NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType; + +/** + * Initializes an attachment with data. + * + * @param path The path of the file whose contents you want to upload to Sentry. + * @param filename The name of the attachment to display in Sentry. + * @param contentType The content type of the attachment. Default is "application/octet-stream". + * @param attachmentType The type of the attachment. Default is "EventAttachment". + */ +- (instancetype)initWithPath:(NSString *)path + filename:(NSString *)filename + contentType:(nullable NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType; + +/** + * The type of the attachment. + */ +@property (readonly, nonatomic) SentryAttachmentType attachmentType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index 73e0ee9859..d21c249445 100644 --- a/Sources/Sentry/include/SentryEnvelope+Private.h +++ b/Sources/Sentry/include/SentryEnvelope+Private.h @@ -1,3 +1,4 @@ +#import "SentryAttachment+Private.h" #import "SentryEnvelope.h" #import diff --git a/Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h b/Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h new file mode 100644 index 0000000000..74faab4d30 --- /dev/null +++ b/Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h @@ -0,0 +1,18 @@ +#import "SentryAttachment+Private.h" +#import "SentryEnvelopeItemHeader.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryEnvelopeAttachmentHeader : SentryEnvelopeItemHeader + +@property (nonatomic, readonly) SentryAttachmentType attachmentType; + +- (instancetype)initWithType:(NSString *)type + length:(NSUInteger)length + filename:(NSString *)filename + contentType:(NSString *)contentType + attachmentType:(SentryAttachmentType)attachmentType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryScope+Private.h b/Sources/Sentry/include/SentryScope+Private.h index fa103c9f1d..c06499ea4a 100644 --- a/Sources/Sentry/include/SentryScope+Private.h +++ b/Sources/Sentry/include/SentryScope+Private.h @@ -27,6 +27,8 @@ SentryScope (Private) - (void)applyToSession:(SentrySession *)session NS_SWIFT_NAME(applyTo(session:)); +- (void)addCrashReportAttachmentInPath:(NSString *)filePath; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryViewHierarchy.h b/Sources/Sentry/include/SentryViewHierarchy.h index cda7447d91..81bc1246e9 100644 --- a/Sources/Sentry/include/SentryViewHierarchy.h +++ b/Sources/Sentry/include/SentryViewHierarchy.h @@ -6,9 +6,9 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryViewHierarchy : NSObject -- (NSArray *)fetchViewHierarchy; +- (nullable NSData *)fetchViewHierarchy; -- (void)saveViewHierarchy:(NSString *)path; +- (BOOL)saveViewHierarchy:(NSString *)filePath; @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift b/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift index fb8c18e65e..b8f2f0b56d 100644 --- a/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ViewHierarchy/SentryViewHierarchyIntegrationTests.swift @@ -9,7 +9,7 @@ class SentryViewHierarchyIntegrationTests: XCTestCase { init() { let testViewHierarchy = TestSentryViewHierarchy() - testViewHierarchy.result = ["view hierarchy"] + testViewHierarchy.result = "view hierarchy".data(using: .utf8) ?? Data() viewHierarchy = testViewHierarchy } @@ -52,6 +52,17 @@ class SentryViewHierarchyIntegrationTests: XCTestCase { XCTAssertFalse(sentrycrash_hasSaveViewHierarchyCallback()) } + func test_processAttachments() { + let sut = fixture.getSut() + let event = Event(error: NSError(domain: "", code: -1)) + + let newAttachmentList = sut.processAttachments([], for: event) + + XCTAssertEqual(newAttachmentList?.first?.filename, "view-hierarchy.json") + XCTAssertEqual(newAttachmentList?.first?.contentType, "application/json") + XCTAssertEqual(newAttachmentList?.first?.attachmentType, .viewHierarchy) + } + func test_noViewHierarchy_attachment() { let sut = fixture.getSut() let event = Event() @@ -82,25 +93,5 @@ class SentryViewHierarchyIntegrationTests: XCTestCase { XCTAssertEqual(newAttachmentList?.count, 1) XCTAssertEqual(newAttachmentList?.first, attachment) } - - func test_attachments() { - let sut = fixture.getSut() - let event = Event(error: NSError(domain: "", code: -1)) - fixture.viewHierarchy.result = ["view hierarchy for window zero", "view hierarchy for window one"] - - let newAttachmentList = sut.processAttachments([], for: event) ?? [] - - XCTAssertEqual(newAttachmentList.count, 2) - XCTAssertEqual(newAttachmentList[0].filename, "view-hierarchy-0.txt") - XCTAssertEqual(newAttachmentList[1].filename, "view-hierarchy-1.txt") - - XCTAssertEqual(newAttachmentList[0].contentType, "text/plain") - XCTAssertEqual(newAttachmentList[1].contentType, "text/plain") - - XCTAssertEqual(newAttachmentList[0].data?.count, "view hierarchy for window zero".lengthOfBytes(using: .utf8)) - XCTAssertEqual(newAttachmentList[1].data?.count, "view hierarchy for window one".lengthOfBytes(using: .utf8)) - - } - } #endif diff --git a/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.h b/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.h new file mode 100644 index 0000000000..92ef21291e --- /dev/null +++ b/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.h @@ -0,0 +1,13 @@ +#import "SentryCrashJSONCodec.h" +#import "SentryDefines.h" +#import "SentryViewHierarchy.h" + +#if SENTRY_HAS_UIKIT +@interface +SentryViewHierarchy (Test) +- (int)viewHierarchyFromView:(UIView *)view intoContext:(SentryCrashJSONEncodeContext *)context; +- (BOOL)processViewHierarchy:(NSArray *)windows + addFunction:(SentryCrashJSONAddDataFunc)addJSONDataFunc + userData:(void *const)userData; +@end +#endif diff --git a/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.swift b/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.swift index 5796d7a6c7..eb5e714594 100644 --- a/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.swift +++ b/Tests/SentryTests/Integrations/ViewHierarchy/TestSentryViewHierarchy.swift @@ -3,10 +3,25 @@ import Foundation #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) class TestSentryViewHierarchy: SentryViewHierarchy { - var result: [String] = [] + var result: Data? + var viewHierarchyResult: Int32 = 0 + var processViewHierarchyCallback: (() -> Void)? - override func fetch() -> [String] { + override func fetch() -> Data? { + guard let result = self.result + else { + return super.fetch() + } return result } + + override func viewHierarchy(from view: UIView!, into context: UnsafeMutablePointer!) -> Int32 { + return viewHierarchyResult != 0 ? viewHierarchyResult : super.viewHierarchy(from: view, into: context) + } + + override func processViewHierarchy(_ windows: [UIView]!, add addJSONDataFunc: SentryCrashJSONAddDataFunc!, userData: UnsafeMutableRawPointer!) -> Bool { + processViewHierarchyCallback?() + return super .processViewHierarchy(windows, add: addJSONDataFunc, userData: userData) + } } #endif diff --git a/Tests/SentryTests/Protocol/SentryAttachment+Equality.m b/Tests/SentryTests/Protocol/SentryAttachment+Equality.m index 48fe128339..6b6ba6b4ef 100644 --- a/Tests/SentryTests/Protocol/SentryAttachment+Equality.m +++ b/Tests/SentryTests/Protocol/SentryAttachment+Equality.m @@ -1,4 +1,5 @@ #import "SentryAttachment+Equality.h" +#import "SentryAttachment+Private.h" @implementation SentryAttachment (Equality) @@ -19,6 +20,8 @@ - (BOOL)isEqualToAttachment:(SentryAttachment *)attachment return YES; if (attachment == nil) return NO; + if (self.attachmentType != attachment.attachmentType) + return NO; if (self.data != attachment.data && ![self.data isEqualToData:attachment.data]) return NO; if (self.path != attachment.path && ![self.path isEqualToString:attachment.path]) @@ -38,6 +41,7 @@ - (NSUInteger)hash hash = hash * 23 + [self.path hash]; hash = hash * 23 + [self.filename hash]; hash = hash * 23 + [self.contentType hash]; + hash = hash * 23 + self.attachmentType; return hash; } diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index b60a20e254..37e025af7d 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -245,12 +245,32 @@ class SentryEnvelopeTests: XCTestCase { let attachment = Attachment(path: fixture.path) let envelopeItem = SentryEnvelopeItem(attachment: attachment, maxAttachmentSize: fixture.maxAttachmentSize)! - + + guard let header = envelopeItem.header as? SentryEnvelopeAttachmentHeader else { + XCTFail("Header should be SentryEnvelopeAttachmentHeader") + return + } + + XCTAssertEqual(header.attachmentType, .eventAttachment) XCTAssertEqual("attachment", envelopeItem.header.type) XCTAssertEqual(UInt(fixture.data?.count ?? 0), envelopeItem.header.length) XCTAssertEqual(attachment.filename, envelopeItem.header.filename) XCTAssertEqual(attachment.contentType, envelopeItem.header.contentType) } + + func testInitWith_ViewHierarchy_Attachment() { + writeDataToFile(data: fixture.data ?? Data()) + + let attachment = Attachment(path: fixture.path, filename: "filename", contentType: "text", attachmentType: .viewHierarchy) + + let envelopeItem = SentryEnvelopeItem(attachment: attachment, maxAttachmentSize: fixture.maxAttachmentSize)! + guard let header = envelopeItem.header as? SentryEnvelopeAttachmentHeader else { + XCTFail("Header should be SentryEnvelopeAttachmentHeader") + return + } + + XCTAssertEqual(header.attachmentType, .viewHierarchy) + } func testInitWithNonExistentFileAttachment() { let attachment = Attachment(path: fixture.path) @@ -267,6 +287,48 @@ class SentryEnvelopeTests: XCTestCase { writeDataToFile(data: fixture.dataTooBig) XCTAssertNil(SentryEnvelopeItem(attachment: Attachment(path: fixture.path), maxAttachmentSize: fixture.maxAttachmentSize)) } + + func test_SentryEnvelopeAttachmentHeaderSerialization() { + let header = SentryEnvelopeAttachmentHeader(type: "SomeType", length: 10, filename: "SomeFileName", contentType: "SomeContentType", attachmentType: .viewHierarchy) + + let data = header.serialize() + XCTAssertEqual(data["type"] as? String, "SomeType") + XCTAssertEqual(data["length"] as? Int, 10) + XCTAssertEqual(data["filename"] as? String, "SomeFileName") + XCTAssertEqual(data["content_type"] as? String, "SomeContentType") + XCTAssertEqual(data["attachment_type"] as? String, "event.view_hierarchy") + XCTAssertEqual(data.count, 5) + + let header2 = SentryEnvelopeAttachmentHeader(type: "SomeType", length: 10) + + let data2 = header2.serialize() + XCTAssertEqual(data2["type"] as? String, "SomeType") + XCTAssertEqual(data2["length"] as? Int, 10) + XCTAssertNil(data2["filename"]) + XCTAssertNil(data2["content_type"]) + XCTAssertEqual(data2["attachment_type"] as? String, "event.attachment") + XCTAssertEqual(data2.count, 3) + } + + func test_SentryEnvelopeItemHeaderSerialization() { + let header = SentryEnvelopeItemHeader(type: "SomeType", length: 10, filenname: "SomeFileName", contentType: "SomeContentType") + + let data = header.serialize() + XCTAssertEqual(data["type"] as? String, "SomeType") + XCTAssertEqual(data["length"] as? Int, 10) + XCTAssertEqual(data["filename"] as? String, "SomeFileName") + XCTAssertEqual(data["content_type"] as? String, "SomeContentType") + XCTAssertEqual(data.count, 4) + + let header2 = SentryEnvelopeItemHeader(type: "SomeType", length: 10) + + let data2 = header2.serialize() + XCTAssertEqual(data2.count, 2) + XCTAssertEqual(data2["type"] as? String, "SomeType") + XCTAssertEqual(data2["length"] as? Int, 10) + XCTAssertNil(data2["filename"]) + XCTAssertNil(data2["content_type"]) + } func testInitWithDataAttachment_MaxAttachmentSize() { let attachmentTooBig = Attachment(data: fixture.dataTooBig, filename: "") diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 57857cf6f6..81847615fb 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1259,6 +1259,20 @@ class SentryClientTest: XCTestCase { XCTAssertNil(fixture.transportAdapter.sendEventWithTraceStateInvocations.first?.traceContext) } + + func test_AddCrashReportAttacment_withViewHierarchy() { + let scope = Scope() + + let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("view-hierarchy.json") + try? "data".data(using: .utf8)?.write(to: tempFile) + + scope.addCrashReportAttachment(inPath: tempFile.path) + + XCTAssertEqual(scope.attachments.count, 1) + XCTAssertEqual(scope.attachments.first?.filename, "view-hierarchy.json") + XCTAssertEqual(scope.attachments.first?.contentType, "application/json") + XCTAssertEqual(scope.attachments.first?.attachmentType, .viewHierarchy) + } func testCaptureEvent_withAdditionalEnvelopeItem() { let event = Event(level: SentryLevel.warning) diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 44700a366b..1e0d45a5a9 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -15,7 +15,7 @@ #import "SentryAppStartTrackingIntegration.h" #import "SentryAppState.h" #import "SentryAppStateManager.h" -#import "SentryAttachment.h" +#import "SentryAttachment+Private.h" #import "SentryAutoBreadcrumbTrackingIntegration+Test.h" #import "SentryAutoBreadcrumbTrackingIntegration.h" #import "SentryAutoSessionTrackingIntegration.h" @@ -176,6 +176,8 @@ #import "UIViewController+Sentry.h" #import "URLSessionTaskMock.h" @import SentryPrivate; +#import "SentryEnvelopeAttachmentHeader.h" +#import "TestSentryViewHierarchy.h" #if SENTRY_HAS_UIKIT # import "MockUIScene.h" diff --git a/Tests/SentryTests/SentryViewHierarchyTests.swift b/Tests/SentryTests/SentryViewHierarchyTests.swift index ae5d1ab21a..797b5de7e3 100644 --- a/Tests/SentryTests/SentryViewHierarchyTests.swift +++ b/Tests/SentryTests/SentryViewHierarchyTests.swift @@ -24,25 +24,152 @@ class SentryViewHierarchyTests: XCTestCase { clearTestState() } - func test_Draw_Each_Window() { + func test_Multiple_Window() { let firstWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) let secondWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) fixture.uiApplication.windows = [firstWindow, secondWindow] - let descriptions = self.fixture.sut.fetch() + guard let descriptions = self.fixture.sut.fetch() else { + XCTFail("Could not serialize view hierarchy") + return + } + + let object = try? JSONSerialization.jsonObject(with: descriptions) as? NSDictionary + let windows = object?["windows"] as? NSArray + XCTAssertNotNil(windows) + XCTAssertEqual(windows?.count, 2) + } + + func test_ViewHierarchy_fetch() { + var window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + window.accessibilityIdentifier = "WindowId" + + fixture.uiApplication.windows = [window] + guard let data = self.fixture.sut.fetch() + else { + XCTFail("Could not serialize view hierarchy") + return + } + var descriptions = String(data: data, encoding: .utf8) ?? "" + + XCTAssertEqual(descriptions, "{\"rendering_system\":\"UIKIT\",\"windows\":[{\"type\":\"UIWindow\",\"identifier\":\"WindowId\",\"width\":10,\"height\":10,\"x\":0,\"y\":0,\"alpha\":1,\"visible\":false,\"children\":[]}]}") + + window = UIWindow(frame: CGRect(x: 1, y: 2, width: 20, height: 30)) + window.accessibilityIdentifier = "IdWindow" + + fixture.uiApplication.windows = [window] + + guard let data = self.fixture.sut.fetch() + else { + XCTFail("Could not serialize view hierarchy") + return + } + descriptions = String(data: data, encoding: .utf8) ?? "" + + XCTAssertEqual(descriptions, "{\"rendering_system\":\"UIKIT\",\"windows\":[{\"type\":\"UIWindow\",\"identifier\":\"IdWindow\",\"width\":20,\"height\":30,\"x\":1,\"y\":2,\"alpha\":1,\"visible\":false,\"children\":[]}]}") + } + + func test_Window_with_children() { + let firstWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + let childView = UIView(frame: CGRect(x: 1, y: 1, width: 8, height: 8)) + let secondChildView = UIView(frame: CGRect(x: 2, y: 2, width: 6, height: 6)) + + firstWindow.addSubview(childView) + firstWindow.addSubview(secondChildView) + + fixture.uiApplication.windows = [firstWindow] + + guard let descriptions = self.fixture.sut.fetch() + else { + XCTFail("Could not serialize view hierarchy") + return + } + + let object = try? JSONSerialization.jsonObject(with: descriptions) as? NSDictionary + let window = (object?["windows"] as? NSArray)?.firstObject as? NSDictionary + let children = window?["children"] as? NSArray + + let firstChild = children?.firstObject as? NSDictionary + + XCTAssertEqual(children?.count, 2) + XCTAssertEqual(firstChild?["type"] as? String, "UIView") + } + + func test_ViewHierarchy_with_ViewController() { + let firstWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + let viewController = UIViewController() + firstWindow.rootViewController = viewController + firstWindow.addSubview(viewController.view) + + fixture.uiApplication.windows = [firstWindow] + + guard let descriptions = self.fixture.sut.fetch() + else { + XCTFail("Could not serialize view hierarchy") + return + } + + let object = try? JSONSerialization.jsonObject(with: descriptions) as? NSDictionary + let window = (object?["windows"] as? NSArray)?.firstObject as? NSDictionary + let children = window?["children"] as? NSArray + + let firstChild = children?.firstObject as? NSDictionary + + XCTAssertEqual(firstChild?["view_controller"] as? String, "UIViewController") + } + + func test_ViewHierarchy_save() { + let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + window.accessibilityIdentifier = "WindowId" + + fixture.uiApplication.windows = [window] + + let path = FileManager.default.temporaryDirectory.appendingPathComponent("view.json").path + self.fixture.sut.save(path) - XCTAssertEqual(descriptions.count, 2) + let descriptions = (try? String(contentsOfFile: path)) ?? "" + + XCTAssertEqual(descriptions, "{\"rendering_system\":\"UIKIT\",\"windows\":[{\"type\":\"UIWindow\",\"identifier\":\"WindowId\",\"width\":10,\"height\":10,\"x\":0,\"y\":0,\"alpha\":1,\"visible\":false,\"children\":[]}]}") } - func test_Draw_ViewHierarchy() { + func test_invalidFilePath() { let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + window.accessibilityIdentifier = "WindowId" fixture.uiApplication.windows = [window] - let descriptions = self.fixture.sut.fetch() + XCTAssertFalse(self.fixture.sut.save("")) + } + + func test_invalidSerialization() { + let sut = TestSentryViewHierarchy() + sut.viewHierarchyResult = -1 + let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + window.accessibilityIdentifier = "WindowId" + + fixture.uiApplication.windows = [window] + let result = sut.fetch() + XCTAssertNil(result) + } + + func test_fetchFromBackgroundTest() { + let sut = TestSentryViewHierarchy() + let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + fixture.uiApplication.windows = [window] + + let ex = expectation(description: "Running on Main Thread") + sut.processViewHierarchyCallback = { + ex.fulfill() + XCTAssertTrue(Thread.isMainThread) + } + + let dispatch = DispatchQueue(label: "background") + dispatch.async { + let _ = sut.fetch() + } - XCTAssertTrue(descriptions[0].starts(with: "