From f1f5a449456060d5b8b12213364f77f88ee0a488 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 13:02:54 +0100 Subject: [PATCH 01/38] fix(tests): add swizzling for NSFileManager --- .github/file-filters.yml | 1 + Sentry.xcodeproj/project.pbxproj | 8 ++ .../SentryCoreDataTrackingIntegration.m | 1 - .../Sentry/SentryFileIOTrackingIntegration.m | 3 + Sources/Sentry/SentryNSDataTracker.m | 17 +++++ Sources/Sentry/SentryNSFileManagerSwizzling.m | 73 +++++++++++++++++++ Sources/Sentry/include/SentryNSDataTracker.h | 10 +++ .../include/SentryNSFileManagerSwizzling.h | 19 +++++ .../IO/SentryNSDataTrackerTests.swift | 19 +++++ 9 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 Sources/Sentry/SentryNSFileManagerSwizzling.m create mode 100644 Sources/Sentry/include/SentryNSFileManagerSwizzling.h diff --git a/.github/file-filters.yml b/.github/file-filters.yml index ba988a3059d..8c5fda239e3 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -5,6 +5,7 @@ high_risk_code: &high_risk_code - 'Sources/Sentry/SentryNetworkTracker.m' - 'Sources/Sentry/SentryUIViewControllerSwizzling.m' - 'Sources/Sentry/SentryNSDataSwizzling.m' + - 'Sources/Sentry/SentryNSFileManagerSwizzling.m' - 'Sources/Sentry/SentrySubClassFinder.m' - 'Sources/Sentry/SentryCoreDataSwizzling.m' - 'Sources/Sentry/SentrySwizzleWrapper.m' diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 60898b55ea9..c71a7680a35 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -780,6 +780,8 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; + D4B029162D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */; }; + D4B029182D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */; }; @@ -1855,6 +1857,8 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; + D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; + D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplay.swift; sourceTree = ""; }; @@ -3890,6 +3894,8 @@ D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */, D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */, D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */, + D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */, + D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */, ); name = IO; sourceTree = ""; @@ -4107,6 +4113,7 @@ 03F84D2427DD414C008FE43F /* SentryCompiler.h in Headers */, 631E6D331EBC679C00712345 /* SentryQueueableRequestManager.h in Headers */, 33EB2A922C341300004FED3D /* Sentry.h in Headers */, + D4B029182D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h in Headers */, 7B3398632459C14000BD9C96 /* SentryEnvelopeRateLimit.h in Headers */, 6304360A1EC0595B00C4D3FA /* SentryNSDataUtils.h in Headers */, 7BF9EF7C2722B90E00B5BBEF /* SentryDefaultObjCRuntimeWrapper.h in Headers */, @@ -4718,6 +4725,7 @@ 844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */, D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */, 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, + D4B029162D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m in Sources */, 63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, diff --git a/Sources/Sentry/SentryCoreDataTrackingIntegration.m b/Sources/Sentry/SentryCoreDataTrackingIntegration.m index 02c2f85575a..7cd0d053f9d 100644 --- a/Sources/Sentry/SentryCoreDataTrackingIntegration.m +++ b/Sources/Sentry/SentryCoreDataTrackingIntegration.m @@ -3,7 +3,6 @@ #import "SentryCoreDataTracker.h" #import "SentryDependencyContainer.h" #import "SentryLog.h" -#import "SentryNSDataSwizzling.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions.h" #import "SentryThreadInspector.h" diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index 230acb84662..09a04dd3797 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -1,6 +1,7 @@ #import "SentryFileIOTrackingIntegration.h" #import "SentryLog.h" #import "SentryNSDataSwizzling.h" +#import "SentryNSFileManagerSwizzling.h" #import "SentryOptions.h" @implementation SentryFileIOTrackingIntegration @@ -12,6 +13,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options } [SentryNSDataSwizzling.shared startWithOptions:options]; + [SentryNSFileManagerSwizzling.shared startWithOptions:options]; return YES; } @@ -25,6 +27,7 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { [SentryNSDataSwizzling.shared stop]; + [SentryNSFileManagerSwizzling.shared stop]; } @end diff --git a/Sources/Sentry/SentryNSDataTracker.m b/Sources/Sentry/SentryNSDataTracker.m index 16474e17b57..79103b846fa 100644 --- a/Sources/Sentry/SentryNSDataTracker.m +++ b/Sources/Sentry/SentryNSDataTracker.m @@ -143,6 +143,23 @@ - (NSData *)measureNSDataFromURL:(NSURL *)url return result; } +- (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path + data:(NSData *)data + attributes:(NSDictionary *)attributes + method: + (BOOL (^)(NSString *_Nonnull, NSData *_Nonnull, + NSDictionary *_Nonnull))method +{ + id span = [self startTrackingWritingNSData:data filePath:path]; + + BOOL result = method(path, data, attributes); + + if (span != nil) { + [self finishTrackingNSData:data span:span]; + } + return result; +} + - (nullable id)spanForPath:(NSString *)path operation:(NSString *)operation size:(NSUInteger)size diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m new file mode 100644 index 00000000000..c4f32be6e94 --- /dev/null +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -0,0 +1,73 @@ +#import "SentryNSFileManagerSwizzling.h" +#import "SentryCrashDefaultMachineContextWrapper.h" +#import "SentryCrashMachineContextWrapper.h" +#import "SentryCrashStackEntryMapper.h" +#import "SentryDependencyContainer.h" +#import "SentryInAppLogic.h" +#import "SentryNSDataTracker.h" +#import "SentryNSProcessInfoWrapper.h" +#import "SentryOptions+Private.h" +#import "SentryStacktraceBuilder.h" +#import "SentrySwizzle.h" +#import "SentryThreadInspector.h" +#import +#import + +@interface SentryNSFileManagerSwizzling () + +@property (nonatomic, strong) SentryNSDataTracker *dataTracker; + +@end + +@implementation SentryNSFileManagerSwizzling + ++ (SentryNSFileManagerSwizzling *)shared +{ + static SentryNSFileManagerSwizzling *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); + return instance; +} + +- (void)startWithOptions:(SentryOptions *)options +{ + self.dataTracker = [[SentryNSDataTracker alloc] + initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; + [self.dataTracker enable]; + [SentryNSFileManagerSwizzling swizzleNSFileManager]; +} + +- (void)stop +{ + [self.dataTracker disable]; +} + +// SentrySwizzleInstanceMethod declaration shadows a local variable. The swizzling is working +// fine and we accept this warning. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshadow" ++ (void)swizzleNSFileManager +{ + SEL createFileAtPathContentsAttributes + = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); + SentrySwizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, + SentrySWReturnType(BOOL), + SentrySWArguments( + NSString * path, NSData * data, NSDictionary * attributes), + SentrySWReplacement({ + return [SentryNSFileManagerSwizzling.shared.dataTracker + measureNSFileManagerCreateFileAtPath:path + data:data + attributes:attributes + method:^BOOL(NSString *path, NSData *data, + NSDictionary + *attributes) { + return SentrySWCallOriginal( + path, data, attributes); + }]; + }), + SentrySwizzleModeOncePerClassAndSuperclasses, (void *)createFileAtPathContentsAttributes); +} +#pragma clang diagnostic pop +@end diff --git a/Sources/Sentry/include/SentryNSDataTracker.h b/Sources/Sentry/include/SentryNSDataTracker.h index 830b136c5fe..bdda1db3cef 100644 --- a/Sources/Sentry/include/SentryNSDataTracker.h +++ b/Sources/Sentry/include/SentryNSDataTracker.h @@ -58,6 +58,16 @@ SENTRY_NO_INIT error:(NSError **)error method:(NSData *_Nullable (^)( NSURL *, NSDataReadingOptions, NSError **))method; + +/** + * Measure NSFileManager 'createFileAtPath:contents:attributes::' method. + */ +- (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path + data:(NSData *)data + attributes:(NSDictionary *)attributes + method:(BOOL (^)(NSString *, NSData *, + NSDictionary *))method; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h new file mode 100644 index 00000000000..cbc91a9190b --- /dev/null +++ b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h @@ -0,0 +1,19 @@ +#import "SentryDefines.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SentryOptions; + +@interface SentryNSFileManagerSwizzling : NSObject +SENTRY_NO_INIT + +@property (class, readonly) SentryNSFileManagerSwizzling *shared; + +- (void)startWithOptions:(SentryOptions *)options; + +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift index 9b0324ecc74..32e8b9e806d 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift @@ -275,6 +275,25 @@ class SentryNSDataTrackerTests: XCTestCase { assertDataSpan(span, path: url.path, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) } + func testCreateFile() { + let sut = fixture.getSut() + var methodPath: String? + var methodData: Data? + var methodAttributes: [FileAttributeKey: Any]? + + sut.measureNSFileManagerCreateFile(atPath: fixture.filePath, data: fixture.data, attributes: [ + FileAttributeKey.size: 123 + ], method: { path, data, attributes in + methodPath = path + methodData = data + methodAttributes = attributes + return true + }) + XCTAssertEqual(methodPath, fixture.filePath) + XCTAssertEqual(methodData, fixture.data) + XCTAssertEqual(methodAttributes?[FileAttributeKey.size] as? Int, 123) + } + func testDontTrackSentryFilesRead() { let sut = fixture.getSut() let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) From b196c307c9bca89970580a0c099ef239836effd1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 13:19:44 +0100 Subject: [PATCH 02/38] Update CHANGELOG.MD --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a3a2eef97..fc9f661d6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Fix GraphQL context for HTTP client error tracking (#4567) +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18.0 and macOS 15 (#4546) ### Improvements From c0bb33d494a8225591e501b7e822881698a49783 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 13:19:58 +0100 Subject: [PATCH 03/38] Add iOS 18 and macOS 15 to GH workflow tests --- .github/workflows/test.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43a6f327ee2..206c687629f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,6 +90,13 @@ jobs: test-destination-os: "17.2" device: "iPhone 15" + # iOS 18 + - runs-on: macos-15 + platform: "iOS" + xcode: "16.1" + test-destination-os: "18.2" + device: "iPhone 16" + # We don't run the unit tests on macOS 13 cause we run them on all on GH actions available iOS versions. # The chance of missing a bug solely on tvOS 16 that doesn't occur on iOS, macOS 12 or macOS 14 is minimal. @@ -99,6 +106,12 @@ jobs: xcode: "15.4" test-destination-os: "latest" + # macOS 15 + - runs-on: macos-15 + platform: "macOS" + xcode: "16.1" + test-destination-os: "latest" + # Catalyst. We only test the latest version, as # the risk something breaking on Catalyst and not # on an older iOS or macOS version is low. From bc22ac936c8ccd50b667a6ff8f3679d9bd825129 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 13:20:13 +0100 Subject: [PATCH 04/38] Revert removal of import statement --- Sources/Sentry/SentryCoreDataTrackingIntegration.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Sentry/SentryCoreDataTrackingIntegration.m b/Sources/Sentry/SentryCoreDataTrackingIntegration.m index 7cd0d053f9d..02c2f85575a 100644 --- a/Sources/Sentry/SentryCoreDataTrackingIntegration.m +++ b/Sources/Sentry/SentryCoreDataTrackingIntegration.m @@ -3,6 +3,7 @@ #import "SentryCoreDataTracker.h" #import "SentryDependencyContainer.h" #import "SentryLog.h" +#import "SentryNSDataSwizzling.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions.h" #import "SentryThreadInspector.h" From effcf8e8189a920520caf0f5b36126eed4ca3e96 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 14:29:06 +0100 Subject: [PATCH 05/38] fix: add OS availability check to file manager swizzling --- Sources/Sentry/SentryNSFileManagerSwizzling.m | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index c4f32be6e94..4fe09208cab 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -49,25 +49,31 @@ - (void)stop #pragma clang diagnostic ignored "-Wshadow" + (void)swizzleNSFileManager { - SEL createFileAtPathContentsAttributes - = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); - SentrySwizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, - SentrySWReturnType(BOOL), - SentrySWArguments( - NSString * path, NSData * data, NSDictionary * attributes), - SentrySWReplacement({ - return [SentryNSFileManagerSwizzling.shared.dataTracker - measureNSFileManagerCreateFileAtPath:path - data:data - attributes:attributes - method:^BOOL(NSString *path, NSData *data, - NSDictionary - *attributes) { - return SentrySWCallOriginal( - path, data, attributes); - }]; - }), - SentrySwizzleModeOncePerClassAndSuperclasses, (void *)createFileAtPathContentsAttributes); + // Before iOS 18.0 and macOS 15.0 the NSFileManager used NSData.writeToFile internally, + // which was tracked using swizzling of NSData. This behaviour changed, therefore the + // file manager needs to swizzled for later versions. + if (@available(iOS 18, macOS 15, *)) { + SEL createFileAtPathContentsAttributes + = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); + SentrySwizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, + SentrySWReturnType(BOOL), + SentrySWArguments( + NSString * path, NSData * data, NSDictionary * attributes), + SentrySWReplacement({ + return [SentryNSFileManagerSwizzling.shared.dataTracker + measureNSFileManagerCreateFileAtPath:path + data:data + attributes:attributes + method:^BOOL(NSString *path, NSData *data, + NSDictionary + *attributes) { + return SentrySWCallOriginal( + path, data, attributes); + }]; + }), + SentrySwizzleModeOncePerClassAndSuperclasses, + (void *)createFileAtPathContentsAttributes); + } } #pragma clang diagnostic pop @end From 7bf668cc37c22dc2a161411908902bb18b89b9e0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 15:16:28 +0100 Subject: [PATCH 06/38] add tvOS 18 and mac catalyst to test workflows --- .github/workflows/test.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 206c687629f..a6f8530e563 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,6 +76,8 @@ jobs: matrix: # Can't run tests on watchOS because XCTest is not available include: + # We are running tests on iOS 17 and later, as there were OS-internal changes introduced in succeeding versions. + # iOS 16 - runs-on: macos-13 platform: "iOS" @@ -94,11 +96,12 @@ jobs: - runs-on: macos-15 platform: "iOS" xcode: "16.1" - test-destination-os: "18.2" + test-destination-os: "18.1" device: "iPhone 16" # We don't run the unit tests on macOS 13 cause we run them on all on GH actions available iOS versions. # The chance of missing a bug solely on tvOS 16 that doesn't occur on iOS, macOS 12 or macOS 14 is minimal. + # We are running tests on macOS 14 and later, as there were OS-internal changes introduced in succeeding versions. # macOS 14 - runs-on: macos-14 @@ -112,16 +115,22 @@ jobs: xcode: "16.1" test-destination-os: "latest" - # Catalyst. We only test the latest version, as - # the risk something breaking on Catalyst and not + # Catalyst. We test the latest version, as the risk something breaking on Catalyst and not # on an older iOS or macOS version is low. + # In addition we are running tests on macOS 14, as there were OS-internal changes introduced in succeeding versions. - runs-on: macos-14 platform: "Catalyst" xcode: "15.4" test-destination-os: "latest" + - runs-on: macos-15 + platform: "Catalyst" + xcode: "16.1" + test-destination-os: "latest" + # We don't run the unit tests on tvOS 16 cause we run them on all on GH actions available iOS versions. # The chance of missing a bug solely on tvOS 16 that doesn't occur on iOS, tvOS 15 or tvOS 16 is minimal. + # We are running tests on tvOS 17 and latest, as there were OS-internal changes introduced in succeeding versions. # tvOS 17 - runs-on: macos-14 @@ -129,6 +138,12 @@ jobs: xcode: "15.4" test-destination-os: "17.5" + # tvOS 18 + - runs-on: macos-15 + platform: "tvOS" + xcode: "16.1" + test-destination-os: "18.1" + steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 From 74dd90cc9a19381a419f3b4c94b078a8868165a7 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 12 Dec 2024 17:02:51 +0100 Subject: [PATCH 07/38] rename SentryNSDataTracker to SentryFileIOTracker; add SentryDataWrapper --- Sentry.xcodeproj/project.pbxproj | 28 +- ...yNSDataTracker.m => SentryFileIOTracker.m} | 6 +- Sources/Sentry/SentryNSDataSwizzling.m | 6 +- Sources/Sentry/SentryNSFileManagerSwizzling.m | 6 +- ...yNSDataTracker.h => SentryFileIOTracker.h} | 2 +- Sources/Swift/Helper/SentryDataWrapper.swift | 605 ++++++++++++++++++ ...s.swift => SentryFileIOTrackerTests.swift} | 6 +- ...SentryFileIOTrackingIntegrationObjCTests.m | 2 +- ...SentryFileIOTrackingIntegrationTests.swift | 101 ++- .../SentryTests/SentryTests-Bridging-Header.h | 2 +- 10 files changed, 729 insertions(+), 35 deletions(-) rename Sources/Sentry/{SentryNSDataTracker.m => SentryFileIOTracker.m} (98%) rename Sources/Sentry/include/{SentryNSDataTracker.h => SentryFileIOTracker.h} (98%) create mode 100644 Sources/Swift/Helper/SentryDataWrapper.swift rename Tests/SentryTests/Integrations/Performance/IO/{SentryNSDataTrackerTests.swift => SentryFileIOTrackerTests.swift} (98%) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index c71a7680a35..9c407a58214 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -782,6 +782,7 @@ A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D4B029162D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */; }; D4B029182D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */; }; + D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */; }; @@ -864,7 +865,7 @@ D8739D172BEEA33F007D2F66 /* SentryLevelHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D8739D152BEEA33F007D2F66 /* SentryLevelHelper.h */; }; D8739D182BEEA33F007D2F66 /* SentryLevelHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D8739D162BEEA33F007D2F66 /* SentryLevelHelper.m */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; - D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; + D875ED0B276CC84700422FAC /* SentryFileIOTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */; }; D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; @@ -880,10 +881,10 @@ D8A65B5D2C98656800974B74 /* SentryReplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A65B5C2C98656000974B74 /* SentryReplayView.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; - D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */; }; + D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */; }; D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */; }; D8ACE3CD2762187D00F5A213 /* SentryNSDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */; }; - D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; + D8ACE3CE2762187D00F5A213 /* SentryFileIOTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; D8AE48AE2C577EAB0092A2A6 /* SentryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48AD2C577EAB0092A2A6 /* SentryLog.swift */; }; D8AE48B02C5782EC0092A2A6 /* SentryLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48AF2C5782EC0092A2A6 /* SentryLogOutput.swift */; }; @@ -1859,6 +1860,7 @@ A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; + D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapper.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplay.swift; sourceTree = ""; }; @@ -1947,7 +1949,7 @@ D8739D162BEEA33F007D2F66 /* SentryLevelHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLevelHelper.m; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; - D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; + D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryFileIOTrackerTests.swift; sourceTree = ""; }; D878C6C02BC8048A0039D6A3 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; }; D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactOptions.swift; sourceTree = ""; }; D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExperimentalOptions.swift; sourceTree = ""; }; @@ -1965,10 +1967,10 @@ D8A65B5C2C98656000974B74 /* SentryReplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayView.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; - D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataTracker.m; sourceTree = ""; }; + D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTracker.m; sourceTree = ""; }; D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegration.m; sourceTree = ""; }; D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataSwizzling.h; path = include/SentryNSDataSwizzling.h; sourceTree = ""; }; - D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; + D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTracker.h; path = include/SentryFileIOTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; D8AE48AD2C577EAB0092A2A6 /* SentryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLog.swift; sourceTree = ""; }; D8AE48AF2C5782EC0092A2A6 /* SentryLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogOutput.swift; sourceTree = ""; }; @@ -2114,6 +2116,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( + D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */, @@ -3804,7 +3807,7 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( - D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */, + D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, 7B82722A27A3220A00F4BFF4 /* SentryFileIoTrackingUnitTests.swift */, @@ -3892,8 +3895,8 @@ D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */, D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */, D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */, - D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */, - D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */, + D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */, + D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */, D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */, D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */, ); @@ -4109,7 +4112,7 @@ 7BC3936825B1AB3E004F03D3 /* SentryLevelMapper.h in Headers */, 8E4E7C6E25DAAAFE006AB9E2 /* SentrySpan.h in Headers */, 84DEE8762B69AD6400A7BC17 /* SentryLaunchProfiling.h in Headers */, - D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */, + D8ACE3CE2762187D00F5A213 /* SentryFileIOTracker.h in Headers */, 03F84D2427DD414C008FE43F /* SentryCompiler.h in Headers */, 631E6D331EBC679C00712345 /* SentryQueueableRequestManager.h in Headers */, 33EB2A922C341300004FED3D /* Sentry.h in Headers */, @@ -4580,7 +4583,7 @@ 7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */, 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */, 84CFA4CA2C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift in Sources */, - D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */, + D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */, 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, 51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, @@ -4762,6 +4765,7 @@ 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */, 7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */, 7D65260E237F649E00113EA2 /* SentryScope.m in Sources */, + D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */, 84281C472A57905700EE88F2 /* SentrySample.m in Sources */, 84AC61D329F7541E009EEF61 /* SentryDispatchSourceWrapper.m in Sources */, 62A456E52B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m in Sources */, @@ -5065,7 +5069,7 @@ 7BC6EC14255C415E0059822A /* SentryExceptionTests.swift in Sources */, 7B82722927A319E900F4BFF4 /* SentryAutoSessionTrackingIntegrationTests.swift in Sources */, 62D6B2A72CCA354B004DDBF1 /* SentryUncaughtNSExceptionsTests.swift in Sources */, - D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */, + D875ED0B276CC84700422FAC /* SentryFileIOTrackerTests.swift in Sources */, 62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */, D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */, 7BC6EC04255C235F0059822A /* SentryFrameTests.swift in Sources */, diff --git a/Sources/Sentry/SentryNSDataTracker.m b/Sources/Sentry/SentryFileIOTracker.m similarity index 98% rename from Sources/Sentry/SentryNSDataTracker.m rename to Sources/Sentry/SentryFileIOTracker.m index 79103b846fa..b181a5e15dd 100644 --- a/Sources/Sentry/SentryNSDataTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -1,4 +1,4 @@ -#import "SentryNSDataTracker.h" +#import "SentryFileIOTracker.h" #import "SentryByteCountFormatter.h" #import "SentryClient+Private.h" #import "SentryDependencyContainer.h" @@ -21,7 +21,7 @@ const NSString *SENTRY_TRACKING_COUNTER_KEY = @"SENTRY_TRACKING_COUNTER_KEY"; -@interface SentryNSDataTracker () +@interface SentryFileIOTracker () @property (nonatomic, assign) BOOL isEnabled; @property (nonatomic, strong) NSMutableSet *processingData; @@ -30,7 +30,7 @@ @interface SentryNSDataTracker () @end -@implementation SentryNSDataTracker +@implementation SentryFileIOTracker - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index a40372f1a09..c7ab31f0779 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -3,8 +3,8 @@ #import "SentryCrashMachineContextWrapper.h" #import "SentryCrashStackEntryMapper.h" #import "SentryDependencyContainer.h" +#import "SentryFileIOTracker.h" #import "SentryInAppLogic.h" -#import "SentryNSDataTracker.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions+Private.h" #import "SentryStacktraceBuilder.h" @@ -15,7 +15,7 @@ @interface SentryNSDataSwizzling () -@property (nonatomic, strong) SentryNSDataTracker *dataTracker; +@property (nonatomic, strong) SentryFileIOTracker *dataTracker; @end @@ -31,7 +31,7 @@ + (SentryNSDataSwizzling *)shared - (void)startWithOptions:(SentryOptions *)options { - self.dataTracker = [[SentryNSDataTracker alloc] + self.dataTracker = [[SentryFileIOTracker alloc] initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.dataTracker enable]; diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 4fe09208cab..6d60e973ead 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -3,8 +3,8 @@ #import "SentryCrashMachineContextWrapper.h" #import "SentryCrashStackEntryMapper.h" #import "SentryDependencyContainer.h" +#import "SentryFileIOTracker.h" #import "SentryInAppLogic.h" -#import "SentryNSDataTracker.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions+Private.h" #import "SentryStacktraceBuilder.h" @@ -15,7 +15,7 @@ @interface SentryNSFileManagerSwizzling () -@property (nonatomic, strong) SentryNSDataTracker *dataTracker; +@property (nonatomic, strong) SentryFileIOTracker *dataTracker; @end @@ -31,7 +31,7 @@ + (SentryNSFileManagerSwizzling *)shared - (void)startWithOptions:(SentryOptions *)options { - self.dataTracker = [[SentryNSDataTracker alloc] + self.dataTracker = [[SentryFileIOTracker alloc] initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.dataTracker enable]; diff --git a/Sources/Sentry/include/SentryNSDataTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h similarity index 98% rename from Sources/Sentry/include/SentryNSDataTracker.h rename to Sources/Sentry/include/SentryFileIOTracker.h index bdda1db3cef..9f257a33052 100644 --- a/Sources/Sentry/include/SentryNSDataTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -8,7 +8,7 @@ static NSString *const SENTRY_FILE_READ_OPERATION = @"file.read"; @class SentryThreadInspector, SentryNSProcessInfoWrapper; -@interface SentryNSDataTracker : NSObject +@interface SentryFileIOTracker : NSObject SENTRY_NO_INIT - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift new file mode 100644 index 00000000000..37564e6f3e7 --- /dev/null +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -0,0 +1,605 @@ +// +// SentryDataWrapper.swift +// Sentry +// +// Created by Philip Niedertscheider on 12.12.24. +// Copyright © 2024 Sentry. All rights reserved. +// + +// swiftlint:disable +// TODO: remove this swiftlint:disable + +/// A drop-in replacement for the standard ``Swift.Data`` but with automatic tracking for file I/O operations. +/// +/// This structure is intended to resemble the same method signatures as of ``Swift.Data``. +@available(macOS 15, iOS 18.0, tvOS 15.0, *) +@frozen public struct SentryDataWrapper: Equatable, Hashable, RandomAccessCollection, MutableCollection, RangeReplaceableCollection, MutableDataProtocol, ContiguousBytes, Sendable { + + /// The wrapped data + public private(set) var data: Data + + /// Convenience initializer + public init(data: Data) { + self.data = data + } + + /// Initialize a `SentryDataWrapper` with copied memory content. + /// + /// + /// - parameter bytes: A pointer to the memory. It will be copied. + /// - parameter count: The number of bytes to copy. + public init(bytes: UnsafeRawPointer, count: Int) { + self.data = Data(bytes: bytes, count: count) + } + + /// Initialize a `SentryDataWrapper` with copied memory content. + /// + /// - parameter buffer: A buffer pointer to copy. The size is calculated from `SourceType` and `buffer.count`. + public init(buffer: UnsafeBufferPointer) { + self.data = Data(buffer: buffer) + } + + /// Initialize a `SentryDataWrapper` with copied memory content. + /// + /// - parameter buffer: A buffer pointer to copy. The size is calculated from `SourceType` and `buffer.count`. + public init(buffer: UnsafeMutableBufferPointer) { + self.data = Data(buffer: buffer) + } + + /// Initialize a `SentryDataWrapper` with a repeating byte pattern + /// + /// - parameter repeatedValue: A byte to initialize the pattern + /// - parameter count: The number of bytes the data initially contains initialized to the repeatedValue + public init(repeating repeatedValue: UInt8, count: Int) { + self.data = Data(repeating: repeatedValue, count: count) + } + + /// Initialize a `SentryDataWrapper` with the specified size. + /// + /// This initializer doesn't necessarily allocate the requested memory right away. `SentryDataWrapper` allocates additional memory as needed, so `capacity` simply establishes the initial capacity. When it does allocate the initial memory, though, it allocates the specified amount. + /// + /// This method sets the `count` of the data to 0. + /// + /// If the capacity specified in `capacity` is greater than four memory pages in size, this may round the amount of requested memory up to the nearest full page. + /// + /// - parameter capacity: The size of the data. + public init(capacity: Int) { + self.data = Data(capacity: capacity) + } + + /// Initialize a `SentryDataWrapper` with the specified count of zeroed bytes. + /// + /// - parameter count: The number of bytes the data initially contains. + public init(count: Int) { + self.data = Data(count: count) + } + + /// Initialize an empty `SentryDataWrapper`. + public init() { + self.data = Data() + } + + /// Initialize a `SentryDataWrapper` without copying the bytes. + /// + /// If the result is mutated and is not a unique reference, then the `SentryDataWrapper` will still follow copy-on-write semantics. In this case, the copy will use its own deallocator. Therefore, it is usually best to only use this initializer when you either enforce immutability with `let` or ensure that no other references to the underlying data are formed. + /// - parameter bytes: A pointer to the bytes. + /// - parameter count: The size of the bytes. + /// - parameter deallocator: Specifies the mechanism to free the indicated buffer, or `.none`. + public init(bytesNoCopy bytes: UnsafeMutableRawPointer, count: Int, deallocator: Data.Deallocator) { + self.data = Data(bytesNoCopy: bytes, count: count, deallocator: deallocator) + } + + /// Creates a new instance of a collection containing the elements of a + /// sequence. + /// + /// - Parameter elements: The sequence of elements for the new collection. + /// `elements` must be finite. + public init(_ elements: S) where S: Sequence, S.Element == UInt8 { + self.data = Data(elements) + } + + @available(swift 4.2) + @available(swift, deprecated: 5, message: "use `init(_:)` instead") + public init(bytes elements: S) where S: Sequence, S.Element == UInt8 { + self.data = Data(bytes: elements) + } + + public typealias ReadingOptions = NSData.ReadingOptions + + public typealias WritingOptions = NSData.WritingOptions + + /// Initialize a `SentryDataWrapper` with the contents of a `URL`. + /// + /// - parameter url: The `URL` to read. + /// - parameter options: Options for the read operation. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if `url` cannot be read. + public init(contentsOf url: URL, options: Data.ReadingOptions = []) throws { + // TODO: start file.read via static tracker + self.data = try Data(contentsOf: url, options: options) + // TODO: end file.read via static tracker + } + + /// Prepares the collection to store the specified number of elements, when + /// doing so is appropriate for the underlying type. + /// + /// If you are adding a known number of elements to a collection, use this + /// method to avoid multiple reallocations. A type that conforms to + /// `RangeReplaceableCollection` can choose how to respond when this method + /// is called. Depending on the type, it may make sense to allocate more or + /// less storage than requested, or to take no action at all. + /// + /// - Parameter n: The requested number of elements to store. + public mutating func reserveCapacity(_ minimumCapacity: Int) { + self.data.reserveCapacity(minimumCapacity) + } + + /// The number of bytes in the data. + public var count: Int { + return self.data.count + } + + /// A `BidirectionalCollection` of `DataProtocol` elements which compose a + /// discontiguous buffer of memory. Each region is a contiguous buffer of + /// bytes. + /// + /// The sum of the lengths of the associated regions must equal `self.count` + /// (such that iterating `regions` and iterating `self` produces the same + /// sequence of indices in the same number of index advancements). + public var regions: CollectionOfOne { + return self.data.regions + } + + /// Access the bytes in the data. + /// + /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. + @available(swift, deprecated: 5, message: "use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead") + public func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { + return try self.data.withUnsafeBytes(body) + } + + /// Calls the given closure with the contents of underlying storage. + /// + /// - note: Calling `withUnsafeBytes` multiple times does not guarantee that + /// the same buffer pointer will be passed in every time. + /// - warning: The buffer argument to the body should not be stored or used + /// outside of the lifetime of the call to the closure. + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { + return try self.data.withUnsafeBytes(body) + } + + /// Executes a closure on the sequence’s contiguous storage. + /// + /// This method calls `body(buffer)`, where `buffer` is a pointer to the + /// collection’s contiguous storage. If the contiguous storage doesn't exist, + /// the collection creates it. If the collection doesn’t support an internal + /// representation in a form of contiguous storage, the method doesn’t call + /// `body` --- it immediately returns `nil`. + /// + /// The optimizer can often eliminate bounds- and uniqueness-checking + /// within an algorithm. When that fails, however, invoking the same + /// algorithm on the `buffer` argument may let you trade safety for speed. + /// + /// Successive calls to this method may provide a different pointer on each + /// call. Don't store `buffer` outside of this method. + /// + /// A `Collection` that provides its own implementation of this method + /// must provide contiguous storage to its elements in the same order + /// as they appear in the collection. This guarantees that it's possible to + /// generate contiguous mutable storage to any of its subsequences by slicing + /// `buffer` with a range formed from the distances to the subsequence's + /// `startIndex` and `endIndex`, respectively. + /// + /// - Parameters: + /// - body: A closure that receives an `UnsafeBufferPointer` to the + /// sequence's contiguous storage. + /// - Returns: The value returned from `body`, unless the sequence doesn't + /// support contiguous storage, in which case the method ignores `body` and + /// returns `nil`. + public func withContiguousStorageIfAvailable(_ body: (_ buffer: UnsafeBufferPointer) throws -> ResultType) rethrows -> ResultType? { + return try self.data.withContiguousStorageIfAvailable(body) + } + + /// Mutate the bytes in the data. + /// + /// This function assumes that you are mutating the contents. + /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. + @available(swift, deprecated: 5, message: "use `withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` instead") + public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { + return try self.data.withUnsafeMutableBytes(body) + } + + public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ResultType) rethrows -> ResultType { + return try self.data.withUnsafeMutableBytes(body) + } + + /// Copy the contents of the data to a pointer. + /// + /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. + /// - parameter count: The number of bytes to copy. + /// - warning: This method does not verify that the contents at pointer have enough space to hold `count` bytes. + public func copyBytes(to pointer: UnsafeMutablePointer, count: Int) { + self.data.copyBytes(to: pointer, count: count) + } + + /// Copy a subset of the contents of the data to a pointer. + /// + /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. + /// - parameter range: The range in the `SentryDataWrapper` to copy. + /// - warning: This method does not verify that the contents at pointer have enough space to hold the required number of bytes. + public func copyBytes(to pointer: UnsafeMutablePointer, from range: Range) { + self.data.copyBytes(to: pointer, from: range) + } + + /// + /// This function copies the bytes in `range` from the data into the buffer. If the count of the `range` is greater than `MemoryLayout.stride * buffer.count` then the first N bytes will be copied into the buffer. + /// - precondition: The range must be within the bounds of the data. Otherwise `fatalError` is called. + /// - parameter buffer: A buffer to copy the data into. + /// - parameter range: A range in the data to copy into the buffer. If the range is empty, this function will return 0 without copying anything. If the range is nil, as much data as will fit into `buffer` is copied. + /// - returns: Number of bytes copied into the destination buffer. + public func copyBytes(to buffer: UnsafeMutableBufferPointer, from range: Range? = nil) -> Int { + return self.data.copyBytes(to: buffer, from: range) + } + + /// Enumerate the contents of the data. + /// + /// In some cases, (for example, a `SentryDataWrapper` backed by a `dispatch_data_t`, the bytes may be stored discontinuously. In those cases, this function invokes the closure for each contiguous region of bytes. + /// - parameter block: The closure to invoke for each region of data. You may stop the enumeration by setting the `stop` parameter to `true`. + @available(swift, deprecated: 5, message: "use `regions` or `for-in` instead") + public func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Data.Index, _ stop: inout Bool) -> Void) { + self.data.enumerateBytes(block) + } + + public mutating func append(_ bytes: UnsafePointer, count: Int) { + self.data.append(bytes, count: count) + } + + public mutating func append(_ other: Data) { + self.data.append(other) + } + + /// Append a buffer of bytes to the data. + /// + /// - parameter buffer: The buffer of bytes to append. The size is calculated from `SourceType` and `buffer.count`. + public mutating func append(_ buffer: UnsafeBufferPointer) { + self.data.append(buffer) + } + + public mutating func append(contentsOf bytes: [UInt8]) { + self.data.append(contentsOf: bytes) + } + + /// Adds the elements of a sequence or collection to the end of this + /// collection. + /// + /// The collection being appended to allocates any additional necessary + /// storage to hold the new elements. + /// + /// The following example appends the elements of a `Range` instance to + /// an array of integers: + /// + /// var numbers = [1, 2, 3, 4, 5] + /// numbers.append(contentsOf: 10...15) + /// print(numbers) + /// // Prints "[1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15]" + /// + /// - Parameter newElements: The elements to append to the collection. + /// + /// - Complexity: O(*m*), where *m* is the length of `newElements`. + public mutating func append(contentsOf elements: S) where S: Sequence, S.Element == UInt8 { + self.data.append(contentsOf: elements) + } + + /// Set a region of the data to `0`. + /// + /// If `range` exceeds the bounds of the data, then the data is resized to fit. + /// - parameter range: The range in the data to set to `0`. + public mutating func resetBytes(in range: Range) { + self.data.resetBytes(in: range) + } + + /// Replace a region of bytes in the data with new data. + /// + /// This will resize the data if required, to fit the entire contents of `SentryDataWrapper`. + /// + /// - precondition: The bounds of `subrange` must be valid indices of the collection. + /// - parameter subrange: The range in the data to replace. If `subrange.lowerBound == data.count && subrange.count == 0` then this operation is an append. + /// - parameter data: The replacement data. + public mutating func replaceSubrange(_ subrange: Range, with data: Data) { + self.data.replaceSubrange(subrange, with: data) + } + + /// Replace a region of bytes in the data with new bytes from a buffer. + /// + /// This will resize the data if required, to fit the entire contents of `buffer`. + /// + /// - precondition: The bounds of `subrange` must be valid indices of the collection. + /// - parameter subrange: The range in the data to replace. + /// - parameter buffer: The replacement bytes. + public mutating func replaceSubrange(_ subrange: Range, with buffer: UnsafeBufferPointer) { + self.data.replaceSubrange(subrange, with: buffer) + } + + /// Replace a region of bytes in the data with new bytes from a collection. + /// + /// This will resize the data if required, to fit the entire contents of `newElements`. + /// + /// - precondition: The bounds of `subrange` must be valid indices of the collection. + /// - parameter subrange: The range in the data to replace. + /// - parameter newElements: The replacement bytes. + public mutating func replaceSubrange(_ subrange: Range, with newElements: ByteCollection) where ByteCollection: Collection, ByteCollection.Element == UInt8 { + self.data.replaceSubrange(subrange, with: newElements) + } + + public mutating func replaceSubrange(_ subrange: Range, with bytes: UnsafeRawPointer, count cnt: Int) { + self.data.replaceSubrange(subrange, with: bytes, count: cnt) + } + + /// Return a new copy of the data in a specified range. + /// + /// - parameter range: The range to copy. + public func subdata(in range: Range) -> Data { + return self.data.subdata(in: range) + } + + /// Write the contents of the `SentryDataWrapper` to a location. + /// + /// - parameter url: The location to write the data into. + /// - parameter options: Options for writing the data. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. + public func write(to url: URL, options: Data.WritingOptions = []) throws { + // TODO: begin file.write via static tracker + try self.data.write(to: url, options: options) + // TODO: end file.write via static tracker + } + + /// The hash value for the data. + public func hash(into hasher: inout Hasher) { + self.data.hash(into: &hasher) + } + + public func advanced(by amount: Int) -> Data { + return self.data.advanced(by: amount) + } + + /// Sets or returns the byte at the specified index. + public subscript(index: Data.Index) -> UInt8 { + get { + return self.data[index] + } + set { + self.data[index] = newValue + } + } + + /// Accesses a contiguous subrange of the collection's elements. + /// + /// The accessed slice uses the same indices for the same elements as the + /// original collection uses. Always use the slice's `startIndex` property + /// instead of assuming that its indices start at a particular value. + /// + /// This example demonstrates getting a slice of an array of strings, finding + /// the index of one of the strings in the slice, and then using that index + /// in the original array. + /// + /// let streets = ["Adams", "Bryant", "Channing", "Douglas", "Evarts"] + /// let streetsSlice = streets[2 ..< streets.endIndex] + /// print(streetsSlice) + /// // Prints "["Channing", "Douglas", "Evarts"]" + /// + /// let index = streetsSlice.firstIndex(of: "Evarts") // 4 + /// print(streets[index!]) + /// // Prints "Evarts" + /// + /// - Parameter bounds: A range of the collection's indices. The bounds of + /// the range must be valid indices of the collection. + /// + /// - Complexity: O(1) + public subscript(bounds: Range) -> Data { + get { + return self.data[bounds] + } + set { + self.data[bounds] = newValue + } + } + + public subscript(rangeExpression: R) -> Data where R: RangeExpression, R.Bound: FixedWidthInteger { + get { + self.data[rangeExpression] + } + set { + self.data[rangeExpression] = newValue + } + } + + /// The start `Index` in the data. + public var startIndex: Data.Index { + return self.data.startIndex + } + + /// The end `Index` into the data. + /// + /// This is the "one-past-the-end" position, and will always be equal to the `count`. + public var endIndex: Data.Index { + return self.data.endIndex + } + + /// Returns the position immediately before the given index. + /// + /// - Parameter i: A valid index of the collection. `i` must be greater than + /// `startIndex`. + /// - Returns: The index value immediately before `i`. + public func index(before i: Data.Index) -> Data.Index { + return self.data.index(before: i) + } + + /// Returns the position immediately after the given index. + /// + /// The successor of an index must be well defined. For an index `i` into a + /// collection `c`, calling `c.index(after: i)` returns the same index every + /// time. + /// + /// - Parameter i: A valid index of the collection. `i` must be less than + /// `endIndex`. + /// - Returns: The index value immediately after `i`. + public func index(after i: Data.Index) -> Data.Index { + return self.data.index(after: i) + } + + /// The indices that are valid for subscripting the collection, in ascending + /// order. + /// + /// A collection's `indices` property can hold a strong reference to the + /// collection itself, causing the collection to be nonuniquely referenced. + /// If you mutate the collection while iterating over its indices, a strong + /// reference can result in an unexpected copy of the collection. To avoid + /// the unexpected copy, use the `index(after:)` method starting with + /// `startIndex` to produce indices instead. + /// + /// var c = MyFancyCollection([10, 20, 30, 40, 50]) + /// var i = c.startIndex + /// while i != c.endIndex { + /// c[i] /= 5 + /// i = c.index(after: i) + /// } + /// // c == MyFancyCollection([2, 4, 6, 8, 10]) + public var indices: Range { + return self.data.indices + } + + /// An iterator over the contents of the data. + /// + /// The iterator will increment byte-by-byte. + public func makeIterator() -> Data.Iterator { + return self.data.makeIterator() + } + + /// Find the given `SentryDataWrapper` in the content of this `SentryDataWrapper`. + /// + /// - parameter dataToFind: The data to be searched for. + /// - parameter options: Options for the search. Default value is `[]`. + /// - parameter range: The range of this data in which to perform the search. Default value is `nil`, which means the entire content of this data. + /// - returns: A `Range` specifying the location of the found data, or nil if a match could not be found. + /// - precondition: `range` must be in the bounds of the Data. + public func range(of dataToFind: Data, options: Data.SearchOptions = [], in range: Range? = nil) -> Range? { + return self.data.range(of: dataToFind, options: options, in: range) + } + + /// Returns `true` if the two `SentryDataWrapper` arguments are equal. + public static func == (d1: SentryDataWrapper, d2: SentryDataWrapper) -> Bool { + return d1.data == d2.data + } + + /// The hash value. + /// + /// Hash values are not guaranteed to be equal across different executions of + /// your program. Do not save hash values to use during a future execution. + /// + /// - Important: `hashValue` is deprecated as a `Hashable` requirement. To + /// conform to `Hashable`, implement the `hash(into:)` requirement instead. + /// The compiler provides an implementation for `hashValue` for you. + public var hashValue: Int { + return self.data.hashValue + } + + /// Initialize a `SentryDataWrapper` from a Base-64 encoded String using the given options. + /// + /// Returns nil when the input is not recognized as valid Base-64. + /// - parameter base64String: The string to parse. + /// - parameter options: Encoding options. Default value is `[]`. + public init?(base64Encoded base64String: String, options: Data.Base64DecodingOptions = []) { + guard let data = Data(base64Encoded: base64String, options: options) else { + return nil + } + self.data = data + } + + /// Initialize a `SentryDataWrapper` from a Base-64, UTF-8 encoded `SentryDataWrapper`. + /// + /// Returns nil when the input is not recognized as valid Base-64. + /// + /// - parameter base64Data: Base-64, UTF-8 encoded input data. + /// - parameter options: Decoding options. Default value is `[]`. + public init?(base64Encoded base64Data: Data, options: Data.Base64DecodingOptions = []) { + guard let data = Data(base64Encoded: base64Data, options: options) else { + return nil + } + self.data = data + } + + /// Returns a Base-64 encoded string. + /// + /// - parameter options: The options to use for the encoding. Default value is `[]`. + /// - returns: The Base-64 encoded string. + public func base64EncodedString(options: Data.Base64EncodingOptions = []) -> String { + return self.data.base64EncodedString(options: options) + } + + /// Returns a Base-64 encoded `SentryDataWrapper`. + /// + /// - parameter options: The options to use for the encoding. Default value is `[]`. + /// - returns: The Base-64 encoded data. + public func base64EncodedData(options: Data.Base64EncodingOptions = []) -> Data { + return self.data.base64EncodedData(options: options) + } + + /// Initialize a `SentryDataWrapper` by adopting a reference type. + /// + /// You can use this initializer to create a `struct Data` that wraps a `class NSData`. `struct Data` will use the `class NSData` for all operations. Other initializers (including casting using `as Data`) may choose to hold a reference or not, based on a what is the most efficient representation. + /// + /// If the resulting value is mutated, then `SentryDataWrapper` will invoke the `mutableCopy()` function on the reference to copy the contents. You may customize the behavior of that function if you wish to return a specialized mutable subclass. + /// + /// - parameter reference: The instance of `NSData` that you wish to wrap. This instance will be copied by `struct Data`. + public init(referencing reference: NSData) { + self.data = Data(referencing: reference) + } +} + +@available(macOS 15, iOS 18.0, tvOS 15.0, *) +extension SentryDataWrapper: CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { + + /// A human-readable description for the data. + public var description: String { + self.data.description + } + + /// A human-readable debug description for the data. + public var debugDescription: String { + self.data.debugDescription + } + + /// The custom mirror for this instance. + /// + /// If this type has value semantics, the mirror should be unaffected by + /// subsequent mutations of the instance. + public var customMirror: Mirror { + self.data.customMirror + } +} + +@available(macOS 15, iOS 18.0, tvOS 15.0, *) +extension SentryDataWrapper: Codable { + + /// Creates a new instance by decoding from the given decoder. + /// + /// This initializer throws an error if reading from the decoder fails, or + /// if the data read is corrupted or otherwise invalid. + /// + /// - Parameter decoder: The decoder to read data from. + public init(from decoder: any Decoder) throws { + self.data = try Data(from: decoder) + } + + /// Encodes this value into the given encoder. + /// + /// If the value fails to encode anything, `encoder` will encode an empty + /// keyed container in its place. + /// + /// This function throws an error if any values are invalid for the given + /// encoder's format. + /// + /// - Parameter encoder: The encoder to write data to. + public func encode(to encoder: any Encoder) throws { + try self.data.encode(to: encoder) + } +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift similarity index 98% rename from Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift rename to Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index 32e8b9e806d..ff8b2cda5a2 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -1,7 +1,7 @@ import SentryTestUtils import XCTest -class SentryNSDataTrackerTests: XCTestCase { +class SentryFileIOTrackerTests: XCTestCase { private class Fixture { @@ -12,7 +12,7 @@ class SentryNSDataTrackerTests: XCTestCase { let threadInspector = TestThreadInspector.instance let imageProvider = TestDebugImageProvider() - func getSut() -> SentryNSDataTracker { + func getSut() -> SentryFileIOTracker { imageProvider.debugImages = [TestData.debugImage] SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider @@ -21,7 +21,7 @@ class SentryNSDataTrackerTests: XCTestCase { let processInfoWrapper = TestSentryNSProcessInfoWrapper() processInfoWrapper.overrides.processDirectoryPath = "sentrytest" - let result = SentryNSDataTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + let result = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) SentryDependencyContainer.sharedInstance().dateProvider = dateProvider result.enable() return result diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index 32efa614c91..38db7d1216a 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -1,5 +1,5 @@ #import "SentryByteCountFormatter.h" -#import "SentryNSDataTracker.h" +#import "SentryFileIOTracker.h" #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index b9b145d96bc..14aeb1dad15 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -78,19 +78,61 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func test_Writing_Tracking() { + let expectedSpanCount: Int + if #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 + } SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.write") { + assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL) } } - + func test_WritingWithOption_Tracking() { + let expectedSpanCount: Int + if #available(iOS 18, macOS 15, tvOS 15, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 + } SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.write") { + assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL, options: .atomic) } } - + + func testDataWrapper_Writing_Tracking() throws { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. + // Therefore, the wrapper is only tested with these OS versions. + guard #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) else { + throw XCTSkip("SentryDataWrapper is not tested on this OS version") + } + SentrySDK.start(options: fixture.getOptions()) + assertSpans(1, "file.write") { + try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL) + } + } + + func testDataWrapper_WritingWithOption_Tracking() throws { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. + // Therefore, the wrapper is only tested with these OS versions. + guard #available(iOS 18, macOS 15, tvOS 15, *) else { + throw XCTSkip("SentryDataWrapper is not tested on this OS version") + } + SentrySDK.start(options: fixture.getOptions()) + assertSpans(1, "file.write") { + try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL, options: .atomic) + } + } + func test_ReadingTrackingDisabled_forIOOption() { SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) @@ -116,20 +158,63 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func test_ReadingURL_Tracking() { + let expectedSpanCount: Int + if #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 + } SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { + assertSpans(expectedSpanCount, "file.read") { let _ = try? Data(contentsOf: fixture.fileURL) } } - - func test_ReadingURLWithOption_Tracking() { + + func test_ReadingURLWithOption_Tracking() throws { + let expectedSpanCount: Int + if #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 + // By asserting for it *not* working, we can lock down the expected behaviour and notice + // if it changes again in the future. + expectedSpanCount = 0 + } else { + expectedSpanCount = 1 + } + SentrySDK.start(options: fixture.getOptions()) + assertSpans(expectedSpanCount, "file.read") { + let data = try? Data(contentsOf: fixture.fileURL, options: .uncached) + XCTAssertEqual(data?.count, fixture.data.count) + } + } + + func testDataWrapper_ReadingURL_Tracking() throws { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. + // Therefore, the wrapper is only tested with these OS versions. + guard #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) else { + throw XCTSkip("SentryDataWrapper is not tested on this OS version") + } + SentrySDK.start(options: fixture.getOptions()) + assertSpans(1, "file.read") { + let _ = try? SentryDataWrapper(contentsOf: fixture.fileURL) + } + } + + func testDataWrapper_ReadingURLWithOption_Tracking() throws { + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. + // Therefore, the wrapper is only tested with these OS versions. + guard #available(iOS 18.0, macOS 15.0, tvOS 15.0, *) else { + throw XCTSkip("SentryDataWrapper is not tested on this OS version") + } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { let data = try? Data(contentsOf: fixture.fileURL, options: .uncached) XCTAssertEqual(data?.count, fixture.data.count) } } - + func test_ReadingFile_Tracking() { SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index a29c3704325..bea62bc7728 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -116,6 +116,7 @@ #import "SentryEnvelopeRateLimit.h" #import "SentryEvent+Private.h" #import "SentryExtraContextProvider.h" +#import "SentryFileIOTracker.h" #import "SentryFileIOTrackingIntegration.h" #import "SentryFileManager+Test.h" #import "SentryFileManager.h" @@ -144,7 +145,6 @@ #import "SentryMeta.h" #import "SentryMigrateSessionInit.h" #import "SentryMsgPackSerializer.h" -#import "SentryNSDataTracker.h" #import "SentryNSDataUtils.h" #import "SentryNSError.h" #import "SentryNSNotificationCenterWrapper.h" From 123871d6a02429f80b4acb3c0ba57005d4b25d32 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 11:57:24 +0100 Subject: [PATCH 08/38] revert changes after splitting PR --- .github/file-filters.yml | 1 - CHANGELOG.md | 2 +- Sentry.xcodeproj/project.pbxproj | 32 +- .../Sentry/SentryFileIOTrackingIntegration.m | 3 - Sources/Sentry/SentryNSDataSwizzling.m | 6 +- ...yFileIOTracker.m => SentryNSDataTracker.m} | 6 +- ...yFileIOTracker.h => SentryNSDataTracker.h} | 12 +- .../include/SentryNSFileManagerSwizzling.h | 19 - Sources/Swift/Helper/SentryDataWrapper.swift | 375 +++--------------- ...SentryFileIOTrackingIntegrationObjCTests.m | 2 +- ...s.swift => SentryNSDataTrackerTests.swift} | 25 +- .../SentryTests/SentryTests-Bridging-Header.h | 2 +- 12 files changed, 88 insertions(+), 397 deletions(-) rename Sources/Sentry/{SentryFileIOTracker.m => SentryNSDataTracker.m} (98%) rename Sources/Sentry/include/{SentryFileIOTracker.h => SentryNSDataTracker.h} (80%) delete mode 100644 Sources/Sentry/include/SentryNSFileManagerSwizzling.h rename Tests/SentryTests/Integrations/Performance/IO/{SentryFileIOTrackerTests.swift => SentryNSDataTrackerTests.swift} (93%) diff --git a/.github/file-filters.yml b/.github/file-filters.yml index 8c5fda239e3..ba988a3059d 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -5,7 +5,6 @@ high_risk_code: &high_risk_code - 'Sources/Sentry/SentryNetworkTracker.m' - 'Sources/Sentry/SentryUIViewControllerSwizzling.m' - 'Sources/Sentry/SentryNSDataSwizzling.m' - - 'Sources/Sentry/SentryNSFileManagerSwizzling.m' - 'Sources/Sentry/SentrySubClassFinder.m' - 'Sources/Sentry/SentryCoreDataSwizzling.m' - 'Sources/Sentry/SentrySwizzleWrapper.m' diff --git a/CHANGELOG.md b/CHANGELOG.md index c957ebb81d5..e6706607852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,13 @@ ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) +- Add file IO tracking wrapper for Swift.Data (#4546) ## 8.42.0-beta.2 ### Fixes - Fix GraphQL context for HTTP client error tracking (#4567) -- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18.0 and macOS 15 (#4546) ### Improvements diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d2a3e9a795b..aa411d31c98 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -780,8 +780,6 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; - D4B029162D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */; }; - D4B029182D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */; }; D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -865,7 +863,7 @@ D8739D172BEEA33F007D2F66 /* SentryLevelHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D8739D152BEEA33F007D2F66 /* SentryLevelHelper.h */; }; D8739D182BEEA33F007D2F66 /* SentryLevelHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D8739D162BEEA33F007D2F66 /* SentryLevelHelper.m */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; - D875ED0B276CC84700422FAC /* SentryFileIOTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */; }; + D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; @@ -881,10 +879,10 @@ D8A65B5D2C98656800974B74 /* SentryReplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A65B5C2C98656000974B74 /* SentryReplayView.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; - D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */; }; + D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */; }; D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */; }; D8ACE3CD2762187D00F5A213 /* SentryNSDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */; }; - D8ACE3CE2762187D00F5A213 /* SentryFileIOTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */; }; + D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */; }; D8ACE3CF2762187D00F5A213 /* SentryFileIOTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */; }; D8AE48AE2C577EAB0092A2A6 /* SentryLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48AD2C577EAB0092A2A6 /* SentryLog.swift */; }; D8AE48B02C5782EC0092A2A6 /* SentryLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8AE48AF2C5782EC0092A2A6 /* SentryLogOutput.swift */; }; @@ -1858,8 +1856,6 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; - D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; - D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapper.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; @@ -1949,7 +1945,7 @@ D8739D162BEEA33F007D2F66 /* SentryLevelHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryLevelHelper.m; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; - D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryFileIOTrackerTests.swift; sourceTree = ""; }; + D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; D878C6C02BC8048A0039D6A3 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; }; D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactOptions.swift; sourceTree = ""; }; D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExperimentalOptions.swift; sourceTree = ""; }; @@ -1967,10 +1963,10 @@ D8A65B5C2C98656000974B74 /* SentryReplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayView.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; - D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTracker.m; sourceTree = ""; }; + D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataTracker.m; sourceTree = ""; }; D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryFileIOTrackingIntegration.m; sourceTree = ""; }; D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataSwizzling.h; path = include/SentryNSDataSwizzling.h; sourceTree = ""; }; - D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTracker.h; path = include/SentryFileIOTracker.h; sourceTree = ""; }; + D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataTracker.h; path = include/SentryNSDataTracker.h; sourceTree = ""; }; D8ACE3CC2762187D00F5A213 /* SentryFileIOTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryFileIOTrackingIntegration.h; path = include/SentryFileIOTrackingIntegration.h; sourceTree = ""; }; D8AE48AD2C577EAB0092A2A6 /* SentryLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLog.swift; sourceTree = ""; }; D8AE48AF2C5782EC0092A2A6 /* SentryLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogOutput.swift; sourceTree = ""; }; @@ -3807,7 +3803,7 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( - D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, + D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, 7B82722A27A3220A00F4BFF4 /* SentryFileIoTrackingUnitTests.swift */, @@ -3895,10 +3891,8 @@ D8ACE3C62762187200F5A213 /* SentryFileIOTrackingIntegration.m */, D8ACE3CA2762187D00F5A213 /* SentryNSDataSwizzling.h */, D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */, - D8ACE3CB2762187D00F5A213 /* SentryFileIOTracker.h */, - D8ACE3C52762187200F5A213 /* SentryFileIOTracker.m */, - D4B029172D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h */, - D4B029152D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m */, + D8ACE3CB2762187D00F5A213 /* SentryNSDataTracker.h */, + D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */, ); name = IO; sourceTree = ""; @@ -4112,11 +4106,10 @@ 7BC3936825B1AB3E004F03D3 /* SentryLevelMapper.h in Headers */, 8E4E7C6E25DAAAFE006AB9E2 /* SentrySpan.h in Headers */, 84DEE8762B69AD6400A7BC17 /* SentryLaunchProfiling.h in Headers */, - D8ACE3CE2762187D00F5A213 /* SentryFileIOTracker.h in Headers */, + D8ACE3CE2762187D00F5A213 /* SentryNSDataTracker.h in Headers */, 03F84D2427DD414C008FE43F /* SentryCompiler.h in Headers */, 631E6D331EBC679C00712345 /* SentryQueueableRequestManager.h in Headers */, 33EB2A922C341300004FED3D /* Sentry.h in Headers */, - D4B029182D02EDC700AFB358 /* SentryNSFileManagerSwizzling.h in Headers */, 7B3398632459C14000BD9C96 /* SentryEnvelopeRateLimit.h in Headers */, 6304360A1EC0595B00C4D3FA /* SentryNSDataUtils.h in Headers */, 7BF9EF7C2722B90E00B5BBEF /* SentryDefaultObjCRuntimeWrapper.h in Headers */, @@ -4583,7 +4576,7 @@ 7B3B473825D6CC7E00D01640 /* SentryNSError.m in Sources */, 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */, 84CFA4CA2C9DF884008DA5F4 /* SentryUserFeedbackWidget.swift in Sources */, - D8ACE3C82762187200F5A213 /* SentryFileIOTracker.m in Sources */, + D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */, 7BE3C77D2446112C00A38442 /* SentryRateLimitParser.m in Sources */, 51B15F7E2BE88A7C0026A2F2 /* URLSessionTaskHelper.swift in Sources */, D8B088B729C9E3FF00213258 /* SentryTracerConfiguration.m in Sources */, @@ -4728,7 +4721,6 @@ 844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */, D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */, 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, - D4B029162D02EDAF00AFB358 /* SentryNSFileManagerSwizzling.m in Sources */, 63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, 03F84D3827DD4191008FE43F /* SentryBacktrace.cpp in Sources */, @@ -5069,7 +5061,7 @@ 7BC6EC14255C415E0059822A /* SentryExceptionTests.swift in Sources */, 7B82722927A319E900F4BFF4 /* SentryAutoSessionTrackingIntegrationTests.swift in Sources */, 62D6B2A72CCA354B004DDBF1 /* SentryUncaughtNSExceptionsTests.swift in Sources */, - D875ED0B276CC84700422FAC /* SentryFileIOTrackerTests.swift in Sources */, + D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */, 62B86CFC29F052BB008F3947 /* SentryTestLogConfig.m in Sources */, D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */, 7BC6EC04255C235F0059822A /* SentryFrameTests.swift in Sources */, diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index 09a04dd3797..230acb84662 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -1,7 +1,6 @@ #import "SentryFileIOTrackingIntegration.h" #import "SentryLog.h" #import "SentryNSDataSwizzling.h" -#import "SentryNSFileManagerSwizzling.h" #import "SentryOptions.h" @implementation SentryFileIOTrackingIntegration @@ -13,7 +12,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options } [SentryNSDataSwizzling.shared startWithOptions:options]; - [SentryNSFileManagerSwizzling.shared startWithOptions:options]; return YES; } @@ -27,7 +25,6 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { [SentryNSDataSwizzling.shared stop]; - [SentryNSFileManagerSwizzling.shared stop]; } @end diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index c7ab31f0779..a40372f1a09 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -3,8 +3,8 @@ #import "SentryCrashMachineContextWrapper.h" #import "SentryCrashStackEntryMapper.h" #import "SentryDependencyContainer.h" -#import "SentryFileIOTracker.h" #import "SentryInAppLogic.h" +#import "SentryNSDataTracker.h" #import "SentryNSProcessInfoWrapper.h" #import "SentryOptions+Private.h" #import "SentryStacktraceBuilder.h" @@ -15,7 +15,7 @@ @interface SentryNSDataSwizzling () -@property (nonatomic, strong) SentryFileIOTracker *dataTracker; +@property (nonatomic, strong) SentryNSDataTracker *dataTracker; @end @@ -31,7 +31,7 @@ + (SentryNSDataSwizzling *)shared - (void)startWithOptions:(SentryOptions *)options { - self.dataTracker = [[SentryFileIOTracker alloc] + self.dataTracker = [[SentryNSDataTracker alloc] initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.dataTracker enable]; diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryNSDataTracker.m similarity index 98% rename from Sources/Sentry/SentryFileIOTracker.m rename to Sources/Sentry/SentryNSDataTracker.m index b181a5e15dd..79103b846fa 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryNSDataTracker.m @@ -1,4 +1,4 @@ -#import "SentryFileIOTracker.h" +#import "SentryNSDataTracker.h" #import "SentryByteCountFormatter.h" #import "SentryClient+Private.h" #import "SentryDependencyContainer.h" @@ -21,7 +21,7 @@ const NSString *SENTRY_TRACKING_COUNTER_KEY = @"SENTRY_TRACKING_COUNTER_KEY"; -@interface SentryFileIOTracker () +@interface SentryNSDataTracker () @property (nonatomic, assign) BOOL isEnabled; @property (nonatomic, strong) NSMutableSet *processingData; @@ -30,7 +30,7 @@ @interface SentryFileIOTracker () @end -@implementation SentryFileIOTracker +@implementation SentryNSDataTracker - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryNSDataTracker.h similarity index 80% rename from Sources/Sentry/include/SentryFileIOTracker.h rename to Sources/Sentry/include/SentryNSDataTracker.h index 9f257a33052..830b136c5fe 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryNSDataTracker.h @@ -8,7 +8,7 @@ static NSString *const SENTRY_FILE_READ_OPERATION = @"file.read"; @class SentryThreadInspector, SentryNSProcessInfoWrapper; -@interface SentryFileIOTracker : NSObject +@interface SentryNSDataTracker : NSObject SENTRY_NO_INIT - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector @@ -58,16 +58,6 @@ SENTRY_NO_INIT error:(NSError **)error method:(NSData *_Nullable (^)( NSURL *, NSDataReadingOptions, NSError **))method; - -/** - * Measure NSFileManager 'createFileAtPath:contents:attributes::' method. - */ -- (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path - data:(NSData *)data - attributes:(NSDictionary *)attributes - method:(BOOL (^)(NSString *, NSData *, - NSDictionary *))method; - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h deleted file mode 100644 index cbc91a9190b..00000000000 --- a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h +++ /dev/null @@ -1,19 +0,0 @@ -#import "SentryDefines.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@class SentryOptions; - -@interface SentryNSFileManagerSwizzling : NSObject -SENTRY_NO_INIT - -@property (class, readonly) SentryNSFileManagerSwizzling *shared; - -- (void)startWithOptions:(SentryOptions *)options; - -- (void)stop; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift index 6289c9a06d1..be0b4cbc719 100644 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -15,338 +15,192 @@ /// The wrapped data public private(set) var data: Data - /// Convenience initializer + /// Convenience initializer to wrap an existing `Data` instance. public init(data: Data) { self.data = data } - /// Initialize a `SentryDataWrapper` with copied memory content. - /// - /// - /// - parameter bytes: A pointer to the memory. It will be copied. - /// - parameter count: The number of bytes to copy. + /// See `Data.init(bytes:count:)` public init(bytes: UnsafeRawPointer, count: Int) { self.data = Data(bytes: bytes, count: count) } - /// Initialize a `SentryDataWrapper` with copied memory content. - /// - /// - parameter buffer: A buffer pointer to copy. The size is calculated from `SourceType` and `buffer.count`. + /// See `Data.init(buffer:)` public init(buffer: UnsafeBufferPointer) { self.data = Data(buffer: buffer) } - /// Initialize a `SentryDataWrapper` with copied memory content. - /// - /// - parameter buffer: A buffer pointer to copy. The size is calculated from `SourceType` and `buffer.count`. + /// See `Data.init(buffer:)` public init(buffer: UnsafeMutableBufferPointer) { self.data = Data(buffer: buffer) } - /// Initialize a `SentryDataWrapper` with a repeating byte pattern - /// - /// - parameter repeatedValue: A byte to initialize the pattern - /// - parameter count: The number of bytes the data initially contains initialized to the repeatedValue + /// See `Data.init(repeating:count:)` public init(repeating repeatedValue: UInt8, count: Int) { self.data = Data(repeating: repeatedValue, count: count) } - /// Initialize a `SentryDataWrapper` with the specified size. - /// - /// This initializer doesn't necessarily allocate the requested memory right away. `SentryDataWrapper` allocates additional memory as needed, so `capacity` simply establishes the initial capacity. When it does allocate the initial memory, though, it allocates the specified amount. - /// - /// This method sets the `count` of the data to 0. - /// - /// If the capacity specified in `capacity` is greater than four memory pages in size, this may round the amount of requested memory up to the nearest full page. - /// - /// - parameter capacity: The size of the data. + /// See `Data.init(capacity:)` public init(capacity: Int) { self.data = Data(capacity: capacity) } - /// Initialize a `SentryDataWrapper` with the specified count of zeroed bytes. - /// - /// - parameter count: The number of bytes the data initially contains. + /// See `Data.init(count:)` public init(count: Int) { self.data = Data(count: count) } - /// Initialize an empty `SentryDataWrapper`. + /// See `Data.init()` public init() { self.data = Data() } - /// Initialize a `SentryDataWrapper` without copying the bytes. - /// - /// If the result is mutated and is not a unique reference, then the `SentryDataWrapper` will still follow copy-on-write semantics. In this case, the copy will use its own deallocator. Therefore, it is usually best to only use this initializer when you either enforce immutability with `let` or ensure that no other references to the underlying data are formed. - /// - parameter bytes: A pointer to the bytes. - /// - parameter count: The size of the bytes. - /// - parameter deallocator: Specifies the mechanism to free the indicated buffer, or `.none`. + /// See `Data.init(bytesNoCopy:count:deallocator:)` public init(bytesNoCopy bytes: UnsafeMutableRawPointer, count: Int, deallocator: Data.Deallocator) { self.data = Data(bytesNoCopy: bytes, count: count, deallocator: deallocator) } - /// Creates a new instance of a collection containing the elements of a - /// sequence. - /// - /// - Parameter elements: The sequence of elements for the new collection. - /// `elements` must be finite. + /// See `Data.init(_:)` public init(_ elements: S) where S: Sequence, S.Element == UInt8 { self.data = Data(elements) } + /// See `Data.init(bytes:)` @available(swift 4.2) @available(swift, deprecated: 5, message: "use `init(_:)` instead") public init(bytes elements: S) where S: Sequence, S.Element == UInt8 { self.data = Data(bytes: elements) } - public typealias ReadingOptions = NSData.ReadingOptions - - public typealias WritingOptions = NSData.WritingOptions - - /// Initialize a `SentryDataWrapper` with the contents of a `URL`. - /// - /// - parameter url: The `URL` to read. - /// - parameter options: Options for the read operation. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if `url` cannot be read. + /// See `Data.init(contentsOf:options:)` public init(contentsOf url: URL, options: Data.ReadingOptions = []) throws { - // TODO: start file.read via static tracker + // start file.read via static tracker self.data = try Data(contentsOf: url, options: options) - // TODO: end file.read via static tracker - } - - /// Prepares the collection to store the specified number of elements, when - /// doing so is appropriate for the underlying type. - /// - /// If you are adding a known number of elements to a collection, use this - /// method to avoid multiple reallocations. A type that conforms to - /// `RangeReplaceableCollection` can choose how to respond when this method - /// is called. Depending on the type, it may make sense to allocate more or - /// less storage than requested, or to take no action at all. - /// - /// - Parameter n: The requested number of elements to store. + // end file.read via static tracker + } + + /// See `Data.reserveCapacity(_:)` public mutating func reserveCapacity(_ minimumCapacity: Int) { self.data.reserveCapacity(minimumCapacity) } - /// The number of bytes in the data. + /// See `Data.count` public var count: Int { return self.data.count } - /// A `BidirectionalCollection` of `DataProtocol` elements which compose a - /// discontiguous buffer of memory. Each region is a contiguous buffer of - /// bytes. - /// - /// The sum of the lengths of the associated regions must equal `self.count` - /// (such that iterating `regions` and iterating `self` produces the same - /// sequence of indices in the same number of index advancements). + /// See `Data.regions` public var regions: CollectionOfOne { return self.data.regions } - /// Access the bytes in the data. - /// - /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. + /// See `Data.withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` @available(swift, deprecated: 5, message: "use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead") public func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { return try self.data.withUnsafeBytes(body) } - /// Calls the given closure with the contents of underlying storage. - /// - /// - note: Calling `withUnsafeBytes` multiple times does not guarantee that - /// the same buffer pointer will be passed in every time. - /// - warning: The buffer argument to the body should not be stored or used - /// outside of the lifetime of the call to the closure. + /// See `Data.withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { return try self.data.withUnsafeBytes(body) } - /// Executes a closure on the sequence’s contiguous storage. - /// - /// This method calls `body(buffer)`, where `buffer` is a pointer to the - /// collection’s contiguous storage. If the contiguous storage doesn't exist, - /// the collection creates it. If the collection doesn’t support an internal - /// representation in a form of contiguous storage, the method doesn’t call - /// `body` --- it immediately returns `nil`. - /// - /// The optimizer can often eliminate bounds- and uniqueness-checking - /// within an algorithm. When that fails, however, invoking the same - /// algorithm on the `buffer` argument may let you trade safety for speed. - /// - /// Successive calls to this method may provide a different pointer on each - /// call. Don't store `buffer` outside of this method. - /// - /// A `Collection` that provides its own implementation of this method - /// must provide contiguous storage to its elements in the same order - /// as they appear in the collection. This guarantees that it's possible to - /// generate contiguous mutable storage to any of its subsequences by slicing - /// `buffer` with a range formed from the distances to the subsequence's - /// `startIndex` and `endIndex`, respectively. - /// - /// - Parameters: - /// - body: A closure that receives an `UnsafeBufferPointer` to the - /// sequence's contiguous storage. - /// - Returns: The value returned from `body`, unless the sequence doesn't - /// support contiguous storage, in which case the method ignores `body` and - /// returns `nil`. + /// See `Data.withContiguousStorageIfAvailable(_ body: (_ buffer: UnsafeBufferPointer) throws -> ResultType) rethrows -> ResultType?` public func withContiguousStorageIfAvailable(_ body: (_ buffer: UnsafeBufferPointer) throws -> ResultType) rethrows -> ResultType? { return try self.data.withContiguousStorageIfAvailable(body) } - /// Mutate the bytes in the data. - /// - /// This function assumes that you are mutating the contents. - /// - warning: The byte pointer argument should not be stored and used outside of the lifetime of the call to the closure. - @available(swift, deprecated: 5, message: "use `withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` instead") + /// See `Data.withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { return try self.data.withUnsafeMutableBytes(body) } + /// See `Data.withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ResultType) rethrows -> ResultType { return try self.data.withUnsafeMutableBytes(body) } - /// Copy the contents of the data to a pointer. - /// - /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. - /// - parameter count: The number of bytes to copy. - /// - warning: This method does not verify that the contents at pointer have enough space to hold `count` bytes. + /// See `Data.copyBytes(to:count:)` public func copyBytes(to pointer: UnsafeMutablePointer, count: Int) { self.data.copyBytes(to: pointer, count: count) } - /// Copy a subset of the contents of the data to a pointer. - /// - /// - parameter pointer: A pointer to the buffer you wish to copy the bytes into. - /// - parameter range: The range in the `SentryDataWrapper` to copy. - /// - warning: This method does not verify that the contents at pointer have enough space to hold the required number of bytes. + /// See `Data.copyBytes(to:from:)` public func copyBytes(to pointer: UnsafeMutablePointer, from range: Range) { self.data.copyBytes(to: pointer, from: range) } - /// - /// This function copies the bytes in `range` from the data into the buffer. If the count of the `range` is greater than `MemoryLayout.stride * buffer.count` then the first N bytes will be copied into the buffer. - /// - precondition: The range must be within the bounds of the data. Otherwise `fatalError` is called. - /// - parameter buffer: A buffer to copy the data into. - /// - parameter range: A range in the data to copy into the buffer. If the range is empty, this function will return 0 without copying anything. If the range is nil, as much data as will fit into `buffer` is copied. - /// - returns: Number of bytes copied into the destination buffer. + /// See `Data.copyBytes(to:from:)` public func copyBytes(to buffer: UnsafeMutableBufferPointer, from range: Range? = nil) -> Int { return self.data.copyBytes(to: buffer, from: range) } - /// Enumerate the contents of the data. - /// - /// In some cases, (for example, a `SentryDataWrapper` backed by a `dispatch_data_t`, the bytes may be stored discontinuously. In those cases, this function invokes the closure for each contiguous region of bytes. - /// - parameter block: The closure to invoke for each region of data. You may stop the enumeration by setting the `stop` parameter to `true`. + /// See `Data.enumerateBytes(_:)` @available(swift, deprecated: 5, message: "use `regions` or `for-in` instead") public func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Data.Index, _ stop: inout Bool) -> Void) { self.data.enumerateBytes(block) } + /// See `Data.append(_:)` public mutating func append(_ bytes: UnsafePointer, count: Int) { self.data.append(bytes, count: count) } + /// See `Data.append(_:)` public mutating func append(_ other: Data) { self.data.append(other) } - /// Append a buffer of bytes to the data. - /// - /// - parameter buffer: The buffer of bytes to append. The size is calculated from `SourceType` and `buffer.count`. + /// See `Data.append(_ buffer: UnsafeBufferPointer)` public mutating func append(_ buffer: UnsafeBufferPointer) { self.data.append(buffer) } + /// See `Data.append(contentsOf:)` public mutating func append(contentsOf bytes: [UInt8]) { self.data.append(contentsOf: bytes) } - /// Adds the elements of a sequence or collection to the end of this - /// collection. - /// - /// The collection being appended to allocates any additional necessary - /// storage to hold the new elements. - /// - /// The following example appends the elements of a `Range` instance to - /// an array of integers: - /// - /// var numbers = [1, 2, 3, 4, 5] - /// numbers.append(contentsOf: 10...15) - /// print(numbers) - /// // Prints "[1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15]" - /// - /// - Parameter newElements: The elements to append to the collection. - /// - /// - Complexity: O(*m*), where *m* is the length of `newElements`. + /// See `Data.append(contentsOf:)` public mutating func append(contentsOf elements: S) where S: Sequence, S.Element == UInt8 { self.data.append(contentsOf: elements) } - /// Set a region of the data to `0`. - /// - /// If `range` exceeds the bounds of the data, then the data is resized to fit. - /// - parameter range: The range in the data to set to `0`. + /// See `Data.resetBytes(in:)` public mutating func resetBytes(in range: Range) { self.data.resetBytes(in: range) } - /// Replace a region of bytes in the data with new data. - /// - /// This will resize the data if required, to fit the entire contents of `SentryDataWrapper`. - /// - /// - precondition: The bounds of `subrange` must be valid indices of the collection. - /// - parameter subrange: The range in the data to replace. If `subrange.lowerBound == data.count && subrange.count == 0` then this operation is an append. - /// - parameter data: The replacement data. + /// See `Data.replaceSubrange(_:with:)` public mutating func replaceSubrange(_ subrange: Range, with data: Data) { self.data.replaceSubrange(subrange, with: data) } - /// Replace a region of bytes in the data with new bytes from a buffer. - /// - /// This will resize the data if required, to fit the entire contents of `buffer`. - /// - /// - precondition: The bounds of `subrange` must be valid indices of the collection. - /// - parameter subrange: The range in the data to replace. - /// - parameter buffer: The replacement bytes. + /// See `Data.replaceSubrange(_:with:)` public mutating func replaceSubrange(_ subrange: Range, with buffer: UnsafeBufferPointer) { self.data.replaceSubrange(subrange, with: buffer) } - /// Replace a region of bytes in the data with new bytes from a collection. - /// - /// This will resize the data if required, to fit the entire contents of `newElements`. - /// - /// - precondition: The bounds of `subrange` must be valid indices of the collection. - /// - parameter subrange: The range in the data to replace. - /// - parameter newElements: The replacement bytes. + /// See `Data.replaceSubrange(_ subrange: Range, with newElements: ByteCollection) where ByteCollection: Collection, ByteCollection.Element == UInt8` public mutating func replaceSubrange(_ subrange: Range, with newElements: ByteCollection) where ByteCollection: Collection, ByteCollection.Element == UInt8 { self.data.replaceSubrange(subrange, with: newElements) } + /// See `Data.replaceSubrange(_:with:)` public mutating func replaceSubrange(_ subrange: Range, with bytes: UnsafeRawPointer, count cnt: Int) { self.data.replaceSubrange(subrange, with: bytes, count: cnt) } - /// Return a new copy of the data in a specified range. - /// - /// - parameter range: The range to copy. + /// See `Data.subdata(in:)` public func subdata(in range: Range) -> Data { return self.data.subdata(in: range) } - /// Write the contents of the `SentryDataWrapper` to a location. - /// - /// - parameter url: The location to write the data into. - /// - parameter options: Options for writing the data. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. + /// See `Data.write(to:options:)` public func write(to url: URL, options: Data.WritingOptions = []) throws { - // TODO: begin file.write via static tracker + // begin file.write via static tracker try self.data.write(to: url, options: options) - // TODO: end file.write via static tracker + // end file.write via static tracker } /// The hash value for the data. @@ -368,29 +222,7 @@ } } - /// Accesses a contiguous subrange of the collection's elements. - /// - /// The accessed slice uses the same indices for the same elements as the - /// original collection uses. Always use the slice's `startIndex` property - /// instead of assuming that its indices start at a particular value. - /// - /// This example demonstrates getting a slice of an array of strings, finding - /// the index of one of the strings in the slice, and then using that index - /// in the original array. - /// - /// let streets = ["Adams", "Bryant", "Channing", "Douglas", "Evarts"] - /// let streetsSlice = streets[2 ..< streets.endIndex] - /// print(streetsSlice) - /// // Prints "["Channing", "Douglas", "Evarts"]" - /// - /// let index = streetsSlice.firstIndex(of: "Evarts") // 4 - /// print(streets[index!]) - /// // Prints "Evarts" - /// - /// - Parameter bounds: A range of the collection's indices. The bounds of - /// the range must be valid indices of the collection. - /// - /// - Complexity: O(1) + /// See `Data.subscript(bounds:)` public subscript(bounds: Range) -> Data { get { return self.data[bounds] @@ -400,6 +232,7 @@ } } + /// See `Data.subscript(rangeExpression: R) -> Data where R: RangeExpression, R.Bound: FixedWidthInteger` public subscript(rangeExpression: R) -> Data where R: RangeExpression, R.Bound: FixedWidthInteger { get { self.data[rangeExpression] @@ -409,101 +242,52 @@ } } - /// The start `Index` in the data. + /// See `Data.startIndex` public var startIndex: Data.Index { return self.data.startIndex } - /// The end `Index` into the data. - /// - /// This is the "one-past-the-end" position, and will always be equal to the `count`. + /// See `Data.endIndex` public var endIndex: Data.Index { return self.data.endIndex } - /// Returns the position immediately before the given index. - /// - /// - Parameter i: A valid index of the collection. `i` must be greater than - /// `startIndex`. - /// - Returns: The index value immediately before `i`. + /// See `Data.index(before:)` public func index(before i: Data.Index) -> Data.Index { return self.data.index(before: i) } - /// Returns the position immediately after the given index. - /// - /// The successor of an index must be well defined. For an index `i` into a - /// collection `c`, calling `c.index(after: i)` returns the same index every - /// time. - /// - /// - Parameter i: A valid index of the collection. `i` must be less than - /// `endIndex`. - /// - Returns: The index value immediately after `i`. + /// See `Data.index(after:)` public func index(after i: Data.Index) -> Data.Index { return self.data.index(after: i) } - /// The indices that are valid for subscripting the collection, in ascending - /// order. - /// - /// A collection's `indices` property can hold a strong reference to the - /// collection itself, causing the collection to be nonuniquely referenced. - /// If you mutate the collection while iterating over its indices, a strong - /// reference can result in an unexpected copy of the collection. To avoid - /// the unexpected copy, use the `index(after:)` method starting with - /// `startIndex` to produce indices instead. - /// - /// var c = MyFancyCollection([10, 20, 30, 40, 50]) - /// var i = c.startIndex - /// while i != c.endIndex { - /// c[i] /= 5 - /// i = c.index(after: i) - /// } - /// // c == MyFancyCollection([2, 4, 6, 8, 10]) + /// See `Data.indices` public var indices: Range { return self.data.indices } - /// An iterator over the contents of the data. - /// - /// The iterator will increment byte-by-byte. + /// See `Data.makeIterator()` public func makeIterator() -> Data.Iterator { return self.data.makeIterator() } - /// Find the given `SentryDataWrapper` in the content of this `SentryDataWrapper`. - /// - /// - parameter dataToFind: The data to be searched for. - /// - parameter options: Options for the search. Default value is `[]`. - /// - parameter range: The range of this data in which to perform the search. Default value is `nil`, which means the entire content of this data. - /// - returns: A `Range` specifying the location of the found data, or nil if a match could not be found. - /// - precondition: `range` must be in the bounds of the Data. + /// See `Data.range(of:options:in:)` public func range(of dataToFind: Data, options: Data.SearchOptions = [], in range: Range? = nil) -> Range? { return self.data.range(of: dataToFind, options: options, in: range) } - /// Returns `true` if the two `SentryDataWrapper` arguments are equal. + /// See `Data.==` public static func == (d1: SentryDataWrapper, d2: SentryDataWrapper) -> Bool { return d1.data == d2.data } - /// The hash value. - /// - /// Hash values are not guaranteed to be equal across different executions of - /// your program. Do not save hash values to use during a future execution. - /// - /// - Important: `hashValue` is deprecated as a `Hashable` requirement. To - /// conform to `Hashable`, implement the `hash(into:)` requirement instead. - /// The compiler provides an implementation for `hashValue` for you. + /// See `Data.hashValue` public var hashValue: Int { return self.data.hashValue } - /// Initialize a `SentryDataWrapper` from a Base-64 encoded String using the given options. - /// - /// Returns nil when the input is not recognized as valid Base-64. - /// - parameter base64String: The string to parse. - /// - parameter options: Encoding options. Default value is `[]`. + /// See `Data.init?(base64Encoded:options:)` public init?(base64Encoded base64String: String, options: Data.Base64DecodingOptions = []) { guard let data = Data(base64Encoded: base64String, options: options) else { return nil @@ -511,12 +295,7 @@ self.data = data } - /// Initialize a `SentryDataWrapper` from a Base-64, UTF-8 encoded `SentryDataWrapper`. - /// - /// Returns nil when the input is not recognized as valid Base-64. - /// - /// - parameter base64Data: Base-64, UTF-8 encoded input data. - /// - parameter options: Decoding options. Default value is `[]`. + /// See `Data.init?(base64Encoded:options:)` public init?(base64Encoded base64Data: Data, options: Data.Base64DecodingOptions = []) { guard let data = Data(base64Encoded: base64Data, options: options) else { return nil @@ -524,29 +303,17 @@ self.data = data } - /// Returns a Base-64 encoded string. - /// - /// - parameter options: The options to use for the encoding. Default value is `[]`. - /// - returns: The Base-64 encoded string. + /// See `Data.base64EncodedString(options:)` public func base64EncodedString(options: Data.Base64EncodingOptions = []) -> String { return self.data.base64EncodedString(options: options) } - /// Returns a Base-64 encoded `SentryDataWrapper`. - /// - /// - parameter options: The options to use for the encoding. Default value is `[]`. - /// - returns: The Base-64 encoded data. + /// See `Data.base64EncodedData(options:)` public func base64EncodedData(options: Data.Base64EncodingOptions = []) -> Data { return self.data.base64EncodedData(options: options) } - /// Initialize a `SentryDataWrapper` by adopting a reference type. - /// - /// You can use this initializer to create a `struct Data` that wraps a `class NSData`. `struct Data` will use the `class NSData` for all operations. Other initializers (including casting using `as Data`) may choose to hold a reference or not, based on a what is the most efficient representation. - /// - /// If the resulting value is mutated, then `SentryDataWrapper` will invoke the `mutableCopy()` function on the reference to copy the contents. You may customize the behavior of that function if you wish to return a specialized mutable subclass. - /// - /// - parameter reference: The instance of `NSData` that you wish to wrap. This instance will be copied by `struct Data`. + /// See `Data.init(referencing:)` public init(referencing reference: NSData) { self.data = Data(referencing: reference) } @@ -555,20 +322,17 @@ @available(iOS 18, macOS 15, tvOS 18, *) extension SentryDataWrapper: CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { - /// A human-readable description for the data. + /// See `Data.description` public var description: String { self.data.description } - /// A human-readable debug description for the data. + /// See `Data.debugDescription` public var debugDescription: String { self.data.debugDescription } - /// The custom mirror for this instance. - /// - /// If this type has value semantics, the mirror should be unaffected by - /// subsequent mutations of the instance. + /// See `Data.customMirror` public var customMirror: Mirror { self.data.customMirror } @@ -577,25 +341,12 @@ extension SentryDataWrapper: CustomStringConvertible, CustomDebugStringConvertib @available(iOS 18, macOS 15, tvOS 18, *) extension SentryDataWrapper: Codable { - /// Creates a new instance by decoding from the given decoder. - /// - /// This initializer throws an error if reading from the decoder fails, or - /// if the data read is corrupted or otherwise invalid. - /// - /// - Parameter decoder: The decoder to read data from. + /// See `Data.init(from:)` public init(from decoder: any Decoder) throws { self.data = try Data(from: decoder) } - /// Encodes this value into the given encoder. - /// - /// If the value fails to encode anything, `encoder` will encode an empty - /// keyed container in its place. - /// - /// This function throws an error if any values are invalid for the given - /// encoder's format. - /// - /// - Parameter encoder: The encoder to write data to. + /// See `Data.encode(to:)` public func encode(to encoder: any Encoder) throws { try self.data.encode(to: encoder) } diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index fb1facfd94b..1d73b809446 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -1,5 +1,5 @@ #import "SentryByteCountFormatter.h" -#import "SentryFileIOTracker.h" +#import "SentryNSDataTracker.h" #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift similarity index 93% rename from Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift rename to Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift index ff8b2cda5a2..9b0324ecc74 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift @@ -1,7 +1,7 @@ import SentryTestUtils import XCTest -class SentryFileIOTrackerTests: XCTestCase { +class SentryNSDataTrackerTests: XCTestCase { private class Fixture { @@ -12,7 +12,7 @@ class SentryFileIOTrackerTests: XCTestCase { let threadInspector = TestThreadInspector.instance let imageProvider = TestDebugImageProvider() - func getSut() -> SentryFileIOTracker { + func getSut() -> SentryNSDataTracker { imageProvider.debugImages = [TestData.debugImage] SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider @@ -21,7 +21,7 @@ class SentryFileIOTrackerTests: XCTestCase { let processInfoWrapper = TestSentryNSProcessInfoWrapper() processInfoWrapper.overrides.processDirectoryPath = "sentrytest" - let result = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + let result = SentryNSDataTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) SentryDependencyContainer.sharedInstance().dateProvider = dateProvider result.enable() return result @@ -275,25 +275,6 @@ class SentryFileIOTrackerTests: XCTestCase { assertDataSpan(span, path: url.path, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) } - func testCreateFile() { - let sut = fixture.getSut() - var methodPath: String? - var methodData: Data? - var methodAttributes: [FileAttributeKey: Any]? - - sut.measureNSFileManagerCreateFile(atPath: fixture.filePath, data: fixture.data, attributes: [ - FileAttributeKey.size: 123 - ], method: { path, data, attributes in - methodPath = path - methodData = data - methodAttributes = attributes - return true - }) - XCTAssertEqual(methodPath, fixture.filePath) - XCTAssertEqual(methodData, fixture.data) - XCTAssertEqual(methodAttributes?[FileAttributeKey.size] as? Int, 123) - } - func testDontTrackSentryFilesRead() { let sut = fixture.getSut() let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index bea62bc7728..a29c3704325 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -116,7 +116,6 @@ #import "SentryEnvelopeRateLimit.h" #import "SentryEvent+Private.h" #import "SentryExtraContextProvider.h" -#import "SentryFileIOTracker.h" #import "SentryFileIOTrackingIntegration.h" #import "SentryFileManager+Test.h" #import "SentryFileManager.h" @@ -145,6 +144,7 @@ #import "SentryMeta.h" #import "SentryMigrateSessionInit.h" #import "SentryMsgPackSerializer.h" +#import "SentryNSDataTracker.h" #import "SentryNSDataUtils.h" #import "SentryNSError.h" #import "SentryNSNotificationCenterWrapper.h" From e002163e28561f0eb8cef62f1c57c257552d3d1f Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 11:59:23 +0100 Subject: [PATCH 09/38] revert changes after splitting PR --- CHANGELOG.md | 2 +- Sources/Sentry/SentryNSDataTracker.m | 17 ---- Sources/Sentry/SentryNSFileManagerSwizzling.m | 79 ------------------- 3 files changed, 1 insertion(+), 97 deletions(-) delete mode 100644 Sources/Sentry/SentryNSFileManagerSwizzling.m diff --git a/CHANGELOG.md b/CHANGELOG.md index e6706607852..7482ad96e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) -- Add file IO tracking wrapper for Swift.Data (#4546) +- Add file IO tracking wrapper for Swift.Data (#4605) ## 8.42.0-beta.2 diff --git a/Sources/Sentry/SentryNSDataTracker.m b/Sources/Sentry/SentryNSDataTracker.m index 79103b846fa..16474e17b57 100644 --- a/Sources/Sentry/SentryNSDataTracker.m +++ b/Sources/Sentry/SentryNSDataTracker.m @@ -143,23 +143,6 @@ - (NSData *)measureNSDataFromURL:(NSURL *)url return result; } -- (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path - data:(NSData *)data - attributes:(NSDictionary *)attributes - method: - (BOOL (^)(NSString *_Nonnull, NSData *_Nonnull, - NSDictionary *_Nonnull))method -{ - id span = [self startTrackingWritingNSData:data filePath:path]; - - BOOL result = method(path, data, attributes); - - if (span != nil) { - [self finishTrackingNSData:data span:span]; - } - return result; -} - - (nullable id)spanForPath:(NSString *)path operation:(NSString *)operation size:(NSUInteger)size diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m deleted file mode 100644 index 6d60e973ead..00000000000 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ /dev/null @@ -1,79 +0,0 @@ -#import "SentryNSFileManagerSwizzling.h" -#import "SentryCrashDefaultMachineContextWrapper.h" -#import "SentryCrashMachineContextWrapper.h" -#import "SentryCrashStackEntryMapper.h" -#import "SentryDependencyContainer.h" -#import "SentryFileIOTracker.h" -#import "SentryInAppLogic.h" -#import "SentryNSProcessInfoWrapper.h" -#import "SentryOptions+Private.h" -#import "SentryStacktraceBuilder.h" -#import "SentrySwizzle.h" -#import "SentryThreadInspector.h" -#import -#import - -@interface SentryNSFileManagerSwizzling () - -@property (nonatomic, strong) SentryFileIOTracker *dataTracker; - -@end - -@implementation SentryNSFileManagerSwizzling - -+ (SentryNSFileManagerSwizzling *)shared -{ - static SentryNSFileManagerSwizzling *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); - return instance; -} - -- (void)startWithOptions:(SentryOptions *)options -{ - self.dataTracker = [[SentryFileIOTracker alloc] - initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] - processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; - [self.dataTracker enable]; - [SentryNSFileManagerSwizzling swizzleNSFileManager]; -} - -- (void)stop -{ - [self.dataTracker disable]; -} - -// SentrySwizzleInstanceMethod declaration shadows a local variable. The swizzling is working -// fine and we accept this warning. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wshadow" -+ (void)swizzleNSFileManager -{ - // Before iOS 18.0 and macOS 15.0 the NSFileManager used NSData.writeToFile internally, - // which was tracked using swizzling of NSData. This behaviour changed, therefore the - // file manager needs to swizzled for later versions. - if (@available(iOS 18, macOS 15, *)) { - SEL createFileAtPathContentsAttributes - = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); - SentrySwizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, - SentrySWReturnType(BOOL), - SentrySWArguments( - NSString * path, NSData * data, NSDictionary * attributes), - SentrySWReplacement({ - return [SentryNSFileManagerSwizzling.shared.dataTracker - measureNSFileManagerCreateFileAtPath:path - data:data - attributes:attributes - method:^BOOL(NSString *path, NSData *data, - NSDictionary - *attributes) { - return SentrySWCallOriginal( - path, data, attributes); - }]; - }), - SentrySwizzleModeOncePerClassAndSuperclasses, - (void *)createFileAtPathContentsAttributes); - } -} -#pragma clang diagnostic pop -@end From ef74f63238a9f7ecb72762c33a375105443a7ff0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 08:51:41 +0100 Subject: [PATCH 10/38] removed deprecated method --- Sources/Swift/Helper/SentryDataWrapper.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift index be0b4cbc719..9b6427e2c30 100644 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -110,11 +110,6 @@ return try self.data.withContiguousStorageIfAvailable(body) } - /// See `Data.withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` - public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { - return try self.data.withUnsafeMutableBytes(body) - } - /// See `Data.withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ResultType) rethrows -> ResultType { return try self.data.withUnsafeMutableBytes(body) From 11c48066347fc4defc34bdf46b348612ec032b70 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 11:33:44 +0100 Subject: [PATCH 11/38] Remove header Co-authored-by: Dhiogo Brustolin --- Sources/Swift/Helper/SentryDataWrapper.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift index 9b6427e2c30..9e80dcb95ee 100644 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -1,11 +1,3 @@ -// -// SentryDataWrapper.swift -// Sentry -// -// Created by Philip Niedertscheider on 12.12.24. -// Copyright © 2024 Sentry. All rights reserved. -// - /// A drop-in replacement for the standard ``Swift.Data`` but with automatic tracking for file I/O operations. /// /// This structure is intended to resemble the same method signatures as of ``Swift.Data``. From 5dc2ead477a188b7dc9533a67275c3fca397cbdb Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 13:21:59 +0100 Subject: [PATCH 12/38] add WIP for tracking file.read/file.write spans --- Sentry.xcodeproj/project.pbxproj | 8 + Sources/Sentry/include/SentryPrivate.h | 4 + .../Helper/SentryDataWrapper+Tracking.swift | 98 ++++++ Sources/Swift/Helper/SentryDataWrapper.swift | 15 +- .../Helper/SentryDataWrapperTests.swift | 331 ++++++++++++++++++ 5 files changed, 449 insertions(+), 7 deletions(-) create mode 100644 Sources/Swift/Helper/SentryDataWrapper+Tracking.swift create mode 100644 Tests/SentryTests/Helper/SentryDataWrapperTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 9cdffa58f0d..986a0990f34 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -782,6 +782,8 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; + D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; + D43DD5DA2D11A44A005BDA32 /* SentryDataWrapper+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */; }; D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -1860,6 +1862,8 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; + D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; + D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryDataWrapper+Tracking.swift"; sourceTree = ""; }; D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapper.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; @@ -2116,6 +2120,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( + D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */, D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, @@ -3056,6 +3061,7 @@ isa = PBXGroup; children = ( D8AE48BE2C578D540092A2A6 /* SentryLog.swift */, + D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */, 849AC3FF29E0C1FF00889C16 /* SentryFormatterTests.swift */, 7B88F30324BC8E6500ADF90A /* SentrySerializationTests.swift */, 62F4DDA02C04CB9700588890 /* SentryBaggageSerializationTests.swift */, @@ -4711,6 +4717,7 @@ 7BC63F0A28081288009D9E37 /* SentrySwizzleWrapper.m in Sources */, 7B6C5EDC264E8DA80010D138 /* SentryFramesTrackingIntegration.m in Sources */, 849B8F9A2C6E906900148E1F /* SentryUserFeedbackConfiguration.swift in Sources */, + D43DD5DA2D11A44A005BDA32 /* SentryDataWrapper+Tracking.swift in Sources */, 63295AF71EF3C7DB002D4490 /* SentryNSDictionarySanitize.m in Sources */, 7B8ECBFC26498958005FE2EF /* SentryAppStateManager.m in Sources */, 7B2A70DD27D6083D008B0D15 /* SentryThreadWrapper.m in Sources */, @@ -4907,6 +4914,7 @@ 7B98D7EC25FB7C4900C5A389 /* SentryAppStateTests.swift in Sources */, 62A3C7BE2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift in Sources */, 8EE017A126704CD500470616 /* SentryUIViewControllerPerformanceTrackerTests.swift in Sources */, + D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */, 7B5B94352657AD21002E474B /* SentryFramesTrackingIntegrationTests.swift in Sources */, 8431EE5B29ADB8EA00D8DC56 /* SentryTimeTests.m in Sources */, 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */, diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 27c7ad8d640..4c11cd99141 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,10 +1,14 @@ // Sentry internal headers that are needed for swift code; you cannot import headers that depend on // public interfaces here #import "NSLocale+Sentry.h" +#import "SentryByteCountFormatter.h" +#import "SentryClient+Private.h" #import "SentryDispatchQueueWrapper.h" #import "SentryNSDataUtils.h" #import "SentryRandom.h" +#import "SentrySDK+Private.h" #import "SentryTime.h" +#import "SentryTraceOrigins.h" #import "SentryUserAccess.h" // Headers that also import SentryDefines should be at the end of this list diff --git a/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift b/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift new file mode 100644 index 00000000000..1e2b1ed5300 --- /dev/null +++ b/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift @@ -0,0 +1,98 @@ +let SENTRY_TRACE_ORIGIN_AUTO_SWIFT_DATA = "auto.file.swift_data" +let SENTRY_TRACKING_COUNTER_KEY = "SENTRY_TRACKING_COUNTER_KEY" +let SENTRY_FILE_WRITE_OPERATION = "file.write" +let SENTRY_FILE_READ_OPERATION = "file.read" + +@available(iOS 18, macOS 15, tvOS 18, *) +extension SentryDataWrapper { + static func startTracking(readingFileUrl url: URL) -> (any Span)? { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. + guard url.isFileURL else { + return nil + } + return startTracking(readingFilePath: url.path) + } + + static func startTracking(readingFilePath path: String) -> (any Span)? { + let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int ?? 0 + Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count + 1 + + if count > 0 { + return nil + } + + return Self.spanForPath(path: path, operation: SENTRY_FILE_READ_OPERATION, size: 0) + } + + static func startTracking(writingData data: Data, toUrl url: URL, options: Data.WritingOptions) -> (any Span)? { + return startTracking(writingData: data, toPath: url.path, options: options) + } + + static func startTracking(writingData data: Data, toPath path: String, options: Data.WritingOptions) -> (any Span)? { + let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int ?? 0 + Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count + 1 + + if count > 0 { + return nil + } + + return Self.spanForPath( + path: path, + operation: SENTRY_FILE_WRITE_OPERATION, + size: data.count + ) + } + + static func spanForPath(path: String, operation: String, size: Int) -> (any Span)? { + if Self.ignoreFile(atPath: path) { + return nil + } + + var ioSpan: (any Span)? + SentrySDK.currentHub().scope.useSpan { span in + ioSpan = span? + .startChild( + operation: operation, + description: Self + .transactionDescription( + forFileAtPath: path, + withFileSize: size + ) + ) + ioSpan?.origin = SENTRY_TRACE_ORIGIN_AUTO_SWIFT_DATA + } + ioSpan?.setData(value: path, key: "file.path") + + return ioSpan + } + + static func ignoreFile(atPath path: String) -> Bool { + let fileManager = SentrySDK.currentHub().getClient()?.fileManager + return fileManager.sentryPath != nil && path.hasPrefix(fileManager.sentryPath) + } + + static func transactionDescription(forFileAtPath path: String, withFileSize size: Int) -> String { + let lastPathComponent = URL(string: path)?.lastPathComponent ?? "nil" + guard size > 0 else { + return lastPathComponent + } + return String( + format: "%@ (%@)", + lastPathComponent, + SentryByteCountFormatter.bytesCountDescription(size) + ) + } + + static func finishTracking(span: (any Span)?, withData data: Data) { + if let span = span { + span.setData(value: NSNumber(value: data.count), key: "file.size") + span.finish() + } + guard let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int else { + return + } + Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count > 1 ? count - 1 : nil + } +} diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift index 9e80dcb95ee..f0cf0fb419d 100644 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -66,9 +66,9 @@ /// See `Data.init(contentsOf:options:)` public init(contentsOf url: URL, options: Data.ReadingOptions = []) throws { - // start file.read via static tracker + let span = Self.startTracking(readingFileUrl: url) self.data = try Data(contentsOf: url, options: options) - // end file.read via static tracker + Self.finishTracking(span: span, withData: self.data) } /// See `Data.reserveCapacity(_:)` @@ -185,9 +185,9 @@ /// See `Data.write(to:options:)` public func write(to url: URL, options: Data.WritingOptions = []) throws { - // begin file.write via static tracker + let span = Self.startTracking(writingData: data, toUrl: url, options: options) try self.data.write(to: url, options: options) - // end file.write via static tracker + Self.finishTracking(span: span, withData: self.data) } /// The hash value for the data. @@ -195,11 +195,12 @@ self.data.hash(into: &hasher) } + /// See `Data.advanced(by:)` public func advanced(by amount: Int) -> Data { return self.data.advanced(by: amount) } - /// Sets or returns the byte at the specified index. + /// See `Data.subscript(index:)` public subscript(index: Data.Index) -> UInt8 { get { return self.data[index] @@ -311,12 +312,12 @@ extension SentryDataWrapper: CustomStringConvertible, CustomDebugStringConvertib /// See `Data.description` public var description: String { - self.data.description + "SentryDataWrapper(\(self.data.description))" } /// See `Data.debugDescription` public var debugDescription: String { - self.data.debugDescription + "SentryDataWrapper(\(self.data.debugDescription))" } /// See `Data.customMirror` diff --git a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift new file mode 100644 index 00000000000..c53d64eba4d --- /dev/null +++ b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift @@ -0,0 +1,331 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class SentryDataWrapperTests: XCTestCase { + private class Fixture { + + let filePath = "Some Path" + let sentryPath = try! TestFileManager(options: Options()).sentryPath + let dateProvider = TestCurrentDateProvider() + let data = "SOME DATA".data(using: .utf8)! + let threadInspector = TestThreadInspector.instance + let imageProvider = TestDebugImageProvider() + + func getSut() -> SentryNSDataTracker { + imageProvider.debugImages = [TestData.debugImage] + SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider + + threadInspector.allThreads = [TestData.thread2] + + let processInfoWrapper = TestSentryNSProcessInfoWrapper() + processInfoWrapper.overrides.processDirectoryPath = "sentrytest" + + let result = SentryNSDataTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + SentryDependencyContainer.sharedInstance().dateProvider = dateProvider + result.enable() + return result + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + fixture.getSut().enable() + SentrySDK.start { + $0.removeAllIntegrations() + } + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func testWritePathAtomically() { + let sut = fixture.getSut() + var methodPath: String? + var methodAuxiliareFile: Bool? + + var result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { path, useAuxiliareFile -> Bool in + methodPath = path + methodAuxiliareFile = useAuxiliareFile + return false + } + + XCTAssertEqual(fixture.filePath, methodPath) + XCTAssertFalse(methodAuxiliareFile!) + XCTAssertFalse(result) + + result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true) { _, useAuxiliareFile -> Bool in + methodAuxiliareFile = useAuxiliareFile + return true + } + + XCTAssertTrue(methodAuxiliareFile!) + XCTAssertTrue(result) + } + + func testWritePathOptionsError() { + let sut = fixture.getSut() + var methodPath: String? + var methodOptions: NSData.WritingOptions? + var methodError: NSError? + + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { path, writingOption, _ -> Bool in + methodPath = path + methodOptions = writingOption + return true + } + + XCTAssertEqual(fixture.filePath, methodPath) + XCTAssertEqual(methodOptions, .atomic) + + do { + try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting) { _, writingOption, errorPointer -> Bool in + methodOptions = writingOption + errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) + return false + } + } catch { + methodError = error as NSError? + } + + XCTAssertEqual(methodOptions, .withoutOverwriting) + XCTAssertEqual(methodError?.domain, "Test Error") + } + + func testWriteAtomically_CheckTrace() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 4) + return true + } + + assertSpanDuration(span: span, expectedDuration: 4) + assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + } + + func testWriteAtomically_CheckTransaction_DebugImages() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 4) + return true + } + + let transactionEvent = Dynamic(transaction).toTransaction().asObject as? Transaction + + XCTAssertNotNil(transactionEvent?.debugMeta) + XCTAssertTrue(transactionEvent?.debugMeta?.count ?? 0 > 0) + XCTAssertEqual(transactionEvent?.debugMeta?.first, TestData.debugImage) + } + + func testWriteAtomically_CheckTransaction_FilterOut_nonProcessFrames() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + + let stackTrace = SentryStacktrace(frames: [TestData.mainFrame, TestData.testFrame, TestData.outsideFrame], registers: ["register": "one"]) + let thread = SentryThread(threadId: 0) + thread.stacktrace = stackTrace + fixture.threadInspector.allThreads = [thread] + + var span: SentrySpan? + + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + span = self.firstSpan(transaction) as? SentrySpan + XCTAssertFalse(span?.isFinished ?? true) + return true + } + + XCTAssertEqual(span?.frames?.count ?? 0, 2) + XCTAssertEqual(span?.frames?.first, TestData.mainFrame) + XCTAssertEqual(span?.frames?.last, TestData.testFrame) + } + + func testWriteAtomically_Background() { + let sut = self.fixture.getSut() + let expect = expectation(description: "Operation in background thread") + DispatchQueue.global(qos: .default).async { + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false) { _, _ -> Bool in + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 4) + return true + } + + self.assertSpanDuration(span: span, expectedDuration: 4) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: self.fixture.data.count, mainThread: false) + expect.fulfill() + } + + wait(for: [expect], timeout: 0.1) + } + + func testWriteWithOptionsAndError_CheckTrace() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { _, _, _ -> Bool in + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 3) + return true + } + + assertSpanDuration(span: span, expectedDuration: 3) + assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + } + + func testDontTrackSentryFilesWrites() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + let expect = expectation(description: "") + try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic) { _, _, _ -> Bool in + span = self.firstSpan(transaction) + expect.fulfill() + return true + } + + XCTAssertNil(span) + wait(for: [expect], timeout: 0.1) + } + + func testReadFromString() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + var usedPath: String? + + let data = sut.measureNSData(fromFile: fixture.filePath) { path in + span = self.firstSpan(transaction) + usedPath = path + return self.fixture.data + } + + XCTAssertEqual(usedPath, fixture.filePath) + XCTAssertEqual(data?.count, fixture.data.count) + + assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + } + + func testReadFromStringOptionsError() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + var usedPath: String? + var usedOptions: NSData.ReadingOptions? + + let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached) { path, options, _ -> Data in + span = self.firstSpan(transaction) + usedOptions = options + usedPath = path + return self.fixture.data + } + + XCTAssertEqual(usedPath, fixture.filePath) + XCTAssertEqual(data?.count, fixture.data.count) + XCTAssertEqual(usedOptions, .uncached) + + assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + } + + func testReadFromURLOptionsError() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + var usedUrl: URL? + let url = URL(fileURLWithPath: fixture.filePath) + var usedOptions: NSData.ReadingOptions? + + let data = try? sut.measureNSData(from: url, options: .uncached) { url, options, _ in + span = self.firstSpan(transaction) + usedOptions = options + usedUrl = url + return self.fixture.data + } + + XCTAssertEqual(usedUrl, url) + XCTAssertEqual(data?.count, fixture.data.count) + XCTAssertEqual(usedOptions, .uncached) + + assertDataSpan(span, path: url.path, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + } + + func testDontTrackSentryFilesRead() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + let expect = expectation(description: "") + let _ = sut.measureNSData(fromFile: fixture.sentryPath) { _ in + span = self.firstSpan(transaction) + expect.fulfill() + return nil + } + + XCTAssertNil(span) + wait(for: [expect], timeout: 0.1) + } + + // MARK: - Helpers + + private func firstSpan(_ transaction: Span) -> Span? { + let result = Dynamic(transaction).children as [Span]? + return result?.first + } + + private func assertDataSpan(_ span: Span?, path: String, operation: String, size: Int, mainThread: Bool = true ) { + XCTAssertNotNil(span) + XCTAssertEqual(span?.operation, operation) + XCTAssertEqual(span?.origin, "auto.file.ns_data") + XCTAssertTrue(span?.isFinished ?? false) + XCTAssertEqual(span?.data["file.size"] as? Int, size) + XCTAssertEqual(span?.data["file.path"] as? String, path) + XCTAssertEqual(span?.data["blocked_main_thread"] as? Bool ?? false, mainThread) + + if mainThread { + guard let frames = (span as? SentrySpan)?.frames else { + XCTFail("File IO Span in the main thread has no frames") + return + } + XCTAssertEqual(frames.first, TestData.mainFrame) + XCTAssertEqual(frames.last, TestData.testFrame) + } + + let lastComponent = (path as NSString).lastPathComponent + + if operation == SENTRY_FILE_READ_OPERATION { + XCTAssertEqual(span?.spanDescription, lastComponent) + } else { + let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) + XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))") + } + } + + private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval) { + let duration = span?.timestamp?.timeIntervalSince(span!.startTimestamp!) + XCTAssertEqual(duration, expectedDuration) + } + + private func advanceTime(bySeconds: TimeInterval) { + fixture.dateProvider.setDate(date: fixture.dateProvider.date().addingTimeInterval(bySeconds)) + } +} From 7047a61fc9d3084b7c3f4f2dd6dba69452f1a066 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 18 Dec 2024 13:08:39 +0100 Subject: [PATCH 13/38] wip --- Sources/Sentry/include/SentryPrivate.h | 9 +++++---- .../Swift/Helper/SentryDataWrapper+Tracking.swift | 14 +++++++++----- scripts/.clang-format-version | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 4c11cd99141..7abeefb0e4e 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,22 +1,23 @@ // Sentry internal headers that are needed for swift code; you cannot import headers that depend on // public interfaces here #import "NSLocale+Sentry.h" -#import "SentryByteCountFormatter.h" -#import "SentryClient+Private.h" #import "SentryDispatchQueueWrapper.h" #import "SentryNSDataUtils.h" #import "SentryRandom.h" -#import "SentrySDK+Private.h" #import "SentryTime.h" -#import "SentryTraceOrigins.h" #import "SentryUserAccess.h" // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile +#import "SentryByteCountFormatter.h" +#import "SentryClient+Private.h" #import "SentryDateUtil.h" #import "SentryDisplayLinkWrapper.h" +#import "SentryFileManager.h" #import "SentryLevelHelper.h" #import "SentryLogC.h" #import "SentryRandom.h" +#import "SentrySDK+Private.h" #import "SentrySdkInfo.h" #import "SentrySession.h" +#import "SentryTraceOrigins.h" diff --git a/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift b/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift index 1e2b1ed5300..ed379a4f688 100644 --- a/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift +++ b/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift @@ -1,3 +1,5 @@ +@_implementationOnly import _SentryPrivate + let SENTRY_TRACE_ORIGIN_AUTO_SWIFT_DATA = "auto.file.swift_data" let SENTRY_TRACKING_COUNTER_KEY = "SENTRY_TRACKING_COUNTER_KEY" let SENTRY_FILE_WRITE_OPERATION = "file.write" @@ -41,11 +43,11 @@ extension SentryDataWrapper { return Self.spanForPath( path: path, operation: SENTRY_FILE_WRITE_OPERATION, - size: data.count + size: UInt(data.count) ) } - static func spanForPath(path: String, operation: String, size: Int) -> (any Span)? { + static func spanForPath(path: String, operation: String, size: UInt) -> (any Span)? { if Self.ignoreFile(atPath: path) { return nil } @@ -69,11 +71,13 @@ extension SentryDataWrapper { } static func ignoreFile(atPath path: String) -> Bool { - let fileManager = SentrySDK.currentHub().getClient()?.fileManager - return fileManager.sentryPath != nil && path.hasPrefix(fileManager.sentryPath) + guard let client = SentrySDK.currentHub().getClient() else { + return false + } + return path.hasPrefix(client.fileManager.sentryPath) } - static func transactionDescription(forFileAtPath path: String, withFileSize size: Int) -> String { + static func transactionDescription(forFileAtPath path: String, withFileSize size: UInt) -> String { let lastPathComponent = URL(string: path)?.lastPathComponent ?? "nil" guard size > 0 else { return lastPathComponent diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index a027c639c4c..87c0f53ffeb 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -19.1.5 +19.1.6 From 036da29cb8482fc6fe8fab3d61acab9765a9a9b0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 20 Dec 2024 12:08:59 +0100 Subject: [PATCH 14/38] add wip changes to reuse data tracker in Swift --- Sentry.xcodeproj/project.pbxproj | 8 +- Sources/Sentry/SentryFileOperations.m | 10 ++ Sources/Sentry/SentryNSDataTracker.m | 17 ++- Sources/Sentry/include/SentryNSDataTracker.h | 6 +- Sources/Sentry/include/SentryPrivate.h | 2 + Sources/Sentry/include/SentrySpanOperations.h | 14 ++- Sources/Sentry/include/SentryTraceOrigins.h | 1 + .../Helper/SentryDataWrapper+Tracking.swift | 102 ------------------ Sources/Swift/Helper/SentryDataWrapper.swift | 29 +++-- .../Helper/SentryDataWrapperTests.swift | 29 +++-- ...SentryFileIOTrackingIntegrationObjCTests.m | 27 ++--- .../IO/SentryNSDataTrackerTests.swift | 21 ++-- 12 files changed, 112 insertions(+), 154 deletions(-) create mode 100644 Sources/Sentry/SentryFileOperations.m delete mode 100644 Sources/Swift/Helper/SentryDataWrapper+Tracking.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index bd68fe368b3..37a7cf7e7a7 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -787,7 +787,7 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; - D43DD5DA2D11A44A005BDA32 /* SentryDataWrapper+Tracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */; }; + D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */; }; D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -1872,7 +1872,7 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; - D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryDataWrapper+Tracking.swift"; sourceTree = ""; }; + D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileOperations.m; sourceTree = ""; }; D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapper.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; @@ -2130,7 +2130,6 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( - D43DD5D92D11A444005BDA32 /* SentryDataWrapper+Tracking.swift */, D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, @@ -3600,6 +3599,7 @@ 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */, 8E4E7C7325DAAB49006AB9E2 /* SentrySpanProtocol.h */, 7B3B83712833832B0001FDEB /* SentrySpanOperations.h */, + D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */, 622C08D729E546F4002571D4 /* SentryTraceOrigins.h */, 8E4E7C6C25DAAAFE006AB9E2 /* SentrySpan.h */, 84A789092C0E9F5800FF0803 /* SentrySpan+Private.h */, @@ -4737,7 +4737,6 @@ 7BC63F0A28081288009D9E37 /* SentrySwizzleWrapper.m in Sources */, 7B6C5EDC264E8DA80010D138 /* SentryFramesTrackingIntegration.m in Sources */, 849B8F9A2C6E906900148E1F /* SentryUserFeedbackConfiguration.swift in Sources */, - D43DD5DA2D11A44A005BDA32 /* SentryDataWrapper+Tracking.swift in Sources */, 63295AF71EF3C7DB002D4490 /* SentryNSDictionarySanitize.m in Sources */, 7B8ECBFC26498958005FE2EF /* SentryAppStateManager.m in Sources */, 7B2A70DD27D6083D008B0D15 /* SentryThreadWrapper.m in Sources */, @@ -4826,6 +4825,7 @@ 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */, 7DAC589123D8B2E0001CF26B /* SentryGlobalEventProcessor.m in Sources */, 7BBD189E244EC8D200427C76 /* SentryRetryAfterHeaderParser.m in Sources */, + D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */, 63FE711920DA4C1000CDBAE8 /* SentryCrashMachineContext.c in Sources */, 63FE711B20DA4C1000CDBAE8 /* SentryCrashString.c in Sources */, 7B14089824878F950035403D /* SentryCrashStackEntryMapper.m in Sources */, diff --git a/Sources/Sentry/SentryFileOperations.m b/Sources/Sentry/SentryFileOperations.m new file mode 100644 index 00000000000..0abf9b082b0 --- /dev/null +++ b/Sources/Sentry/SentryFileOperations.m @@ -0,0 +1,10 @@ +#import "SentrySpanOperations.h" + +NSString *const SentrySpanOperationFileWrite = @"file.write"; +NSString *const SentrySpanOperationFileRead = @"file.read"; + +NSString *const SentrySpanOperationUILoad = @"ui.load"; +NSString *const SentrySpanOperationUILoadInitialDisplay = @"ui.load.initial_display"; +NSString *const SentrySpanOperationUILoadFullDisplay = @"ui.load.full_display"; +NSString *const SentrySpanOperationUIAction = @"ui.action"; +NSString *const SentrySpanOperationUIActionClick = @"ui.action.click"; diff --git a/Sources/Sentry/SentryNSDataTracker.m b/Sources/Sentry/SentryNSDataTracker.m index 16474e17b57..2f5e46875bb 100644 --- a/Sources/Sentry/SentryNSDataTracker.m +++ b/Sources/Sentry/SentryNSDataTracker.m @@ -12,6 +12,7 @@ #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySpan.h" +#import "SentrySpanOperations.h" #import "SentrySpanProtocol.h" #import "SentryStacktrace.h" #import "SentryThread.h" @@ -19,8 +20,6 @@ #import "SentryTraceOrigins.h" #import "SentryTracer.h" -const NSString *SENTRY_TRACKING_COUNTER_KEY = @"SENTRY_TRACKING_COUNTER_KEY"; - @interface SentryNSDataTracker () @property (nonatomic, assign) BOOL isEnabled; @@ -32,6 +31,16 @@ @interface SentryNSDataTracker () @implementation SentryNSDataTracker +NSString *const SENTRY_TRACKING_COUNTER_KEY = @"SENTRY_TRACKING_COUNTER_KEY"; + ++ (instancetype)sharedInstance +{ + SentryNSDataTracker *tracker = [[SentryNSDataTracker alloc] + initWithThreadInspector:SentryDependencyContainer.sharedInstance.threadInspector + processInfoWrapper:SentryDependencyContainer.sharedInstance.processInfoWrapper]; + return tracker; +} + - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper { @@ -213,7 +222,7 @@ - (void)mainThreadExtraInfo:(id)span - (nullable id)startTrackingWritingNSData:(NSData *)data filePath:(NSString *)path { - return [self spanForPath:path operation:SENTRY_FILE_WRITE_OPERATION size:data.length]; + return [self spanForPath:path operation:SentrySpanOperationFileWrite size:data.length]; } - (nullable id)startTrackingReadingFilePath:(NSString *)path @@ -228,7 +237,7 @@ - (void)mainThreadExtraInfo:(id)span if (count) return nil; - return [self spanForPath:path operation:SENTRY_FILE_READ_OPERATION size:0]; + return [self spanForPath:path operation:SentrySpanOperationFileRead size:0]; } - (void)endTrackingFile diff --git a/Sources/Sentry/include/SentryNSDataTracker.h b/Sources/Sentry/include/SentryNSDataTracker.h index 28294df366e..bd0e98d3084 100644 --- a/Sources/Sentry/include/SentryNSDataTracker.h +++ b/Sources/Sentry/include/SentryNSDataTracker.h @@ -2,15 +2,13 @@ #import NS_ASSUME_NONNULL_BEGIN -static NSString *const SENTRY_FILE_WRITE_OPERATION = @"file.write"; - -static NSString *const SENTRY_FILE_READ_OPERATION = @"file.read"; @class SentryNSProcessInfoWrapper; @class SentryThreadInspector; @interface SentryNSDataTracker : NSObject -SENTRY_NO_INIT + ++ (instancetype)sharedInstance; - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper; diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 7abeefb0e4e..a3bfb8adcad 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -16,8 +16,10 @@ #import "SentryFileManager.h" #import "SentryLevelHelper.h" #import "SentryLogC.h" +#import "SentryNSDataTracker.h" #import "SentryRandom.h" #import "SentrySDK+Private.h" #import "SentrySdkInfo.h" #import "SentrySession.h" +#import "SentrySpanOperations.h" #import "SentryTraceOrigins.h" diff --git a/Sources/Sentry/include/SentrySpanOperations.h b/Sources/Sentry/include/SentrySpanOperations.h index 57eb606c98b..4e5543ab9af 100644 --- a/Sources/Sentry/include/SentrySpanOperations.h +++ b/Sources/Sentry/include/SentrySpanOperations.h @@ -1,7 +1,11 @@ +#import "SentryDefines.h" #import -static NSString *const SentrySpanOperationUILoad = @"ui.load"; -static NSString *const SentrySpanOperationUILoadInitialDisplay = @"ui.load.initial_display"; -static NSString *const SentrySpanOperationUILoadFullDisplay = @"ui.load.full_display"; -static NSString *const SentrySpanOperationUIAction = @"ui.action"; -static NSString *const SentrySpanOperationUIActionClick = @"ui.action.click"; +SENTRY_EXTERN NSString *const SentrySpanOperationFileWrite; +SENTRY_EXTERN NSString *const SentrySpanOperationFileRead; + +SENTRY_EXTERN NSString *const SentrySpanOperationUILoad; +SENTRY_EXTERN NSString *const SentrySpanOperationUILoadInitialDisplay; +SENTRY_EXTERN NSString *const SentrySpanOperationUILoadFullDisplay; +SENTRY_EXTERN NSString *const SentrySpanOperationUIAction; +SENTRY_EXTERN NSString *const SentrySpanOperationUIActionClick; diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 7922cdb9a7c..a95345d494c 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -6,6 +6,7 @@ static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker static NSString *const SentryTraceOriginAutoAppStart = @"auto.app.start"; static NSString *const SentryTraceOriginAutoAppStartProfile = @"auto.app.start.profile"; static NSString *const SentryTraceOriginAutoNSData = @"auto.file.ns_data"; +static NSString *const SentryTraceOriginAutoData = @"auto.file.data"; static NSString *const SentryTraceOriginAutoDBCoreData = @"auto.db.core_data"; static NSString *const SentryTraceOriginAutoHttpNSURLSession = @"auto.http.ns_url_session"; static NSString *const SentryTraceOriginAutoUIViewController = @"auto.ui.view_controller"; diff --git a/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift b/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift deleted file mode 100644 index ed379a4f688..00000000000 --- a/Sources/Swift/Helper/SentryDataWrapper+Tracking.swift +++ /dev/null @@ -1,102 +0,0 @@ -@_implementationOnly import _SentryPrivate - -let SENTRY_TRACE_ORIGIN_AUTO_SWIFT_DATA = "auto.file.swift_data" -let SENTRY_TRACKING_COUNTER_KEY = "SENTRY_TRACKING_COUNTER_KEY" -let SENTRY_FILE_WRITE_OPERATION = "file.write" -let SENTRY_FILE_READ_OPERATION = "file.read" - -@available(iOS 18, macOS 15, tvOS 18, *) -extension SentryDataWrapper { - static func startTracking(readingFileUrl url: URL) -> (any Span)? { - // We dont track reads from a url that is not a file url - // because these reads are handled by NSURLSession and - // SentryNetworkTracker will create spans in these cases. - guard url.isFileURL else { - return nil - } - return startTracking(readingFilePath: url.path) - } - - static func startTracking(readingFilePath path: String) -> (any Span)? { - let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int ?? 0 - Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count + 1 - - if count > 0 { - return nil - } - - return Self.spanForPath(path: path, operation: SENTRY_FILE_READ_OPERATION, size: 0) - } - - static func startTracking(writingData data: Data, toUrl url: URL, options: Data.WritingOptions) -> (any Span)? { - return startTracking(writingData: data, toPath: url.path, options: options) - } - - static func startTracking(writingData data: Data, toPath path: String, options: Data.WritingOptions) -> (any Span)? { - let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int ?? 0 - Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count + 1 - - if count > 0 { - return nil - } - - return Self.spanForPath( - path: path, - operation: SENTRY_FILE_WRITE_OPERATION, - size: UInt(data.count) - ) - } - - static func spanForPath(path: String, operation: String, size: UInt) -> (any Span)? { - if Self.ignoreFile(atPath: path) { - return nil - } - - var ioSpan: (any Span)? - SentrySDK.currentHub().scope.useSpan { span in - ioSpan = span? - .startChild( - operation: operation, - description: Self - .transactionDescription( - forFileAtPath: path, - withFileSize: size - ) - ) - ioSpan?.origin = SENTRY_TRACE_ORIGIN_AUTO_SWIFT_DATA - } - ioSpan?.setData(value: path, key: "file.path") - - return ioSpan - } - - static func ignoreFile(atPath path: String) -> Bool { - guard let client = SentrySDK.currentHub().getClient() else { - return false - } - return path.hasPrefix(client.fileManager.sentryPath) - } - - static func transactionDescription(forFileAtPath path: String, withFileSize size: UInt) -> String { - let lastPathComponent = URL(string: path)?.lastPathComponent ?? "nil" - guard size > 0 else { - return lastPathComponent - } - return String( - format: "%@ (%@)", - lastPathComponent, - SentryByteCountFormatter.bytesCountDescription(size) - ) - } - - static func finishTracking(span: (any Span)?, withData data: Data) { - if let span = span { - span.setData(value: NSNumber(value: data.count), key: "file.size") - span.finish() - } - guard let count = Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] as? Int else { - return - } - Thread.current.threadDictionary[SENTRY_TRACKING_COUNTER_KEY] = count > 1 ? count - 1 : nil - } -} diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift index f0cf0fb419d..4db61752d6d 100644 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ b/Sources/Swift/Helper/SentryDataWrapper.swift @@ -1,3 +1,5 @@ +@_implementationOnly import _SentryPrivate + /// A drop-in replacement for the standard ``Swift.Data`` but with automatic tracking for file I/O operations. /// /// This structure is intended to resemble the same method signatures as of ``Swift.Data``. @@ -66,9 +68,17 @@ /// See `Data.init(contentsOf:options:)` public init(contentsOf url: URL, options: Data.ReadingOptions = []) throws { - let span = Self.startTracking(readingFileUrl: url) - self.data = try Data(contentsOf: url, options: options) - Self.finishTracking(span: span, withData: self.data) + let tracker = SentryNSDataTracker.sharedInstance() + self.data = try tracker.measureNSData( + from: url + ) { url, options, errorPtr in + do { + return try Data(contentsOf: url, options: options) + } catch { + errorPtr?.pointee = error as NSError + return nil + } + } } /// See `Data.reserveCapacity(_:)` @@ -185,9 +195,16 @@ /// See `Data.write(to:options:)` public func write(to url: URL, options: Data.WritingOptions = []) throws { - let span = Self.startTracking(writingData: data, toUrl: url, options: options) - try self.data.write(to: url, options: options) - Self.finishTracking(span: span, withData: self.data) + let tracker = SentryNSDataTracker.sharedInstance() + try tracker.measure(self.data, writeToFile: url.path) { _, options, errorPtr in + do { + try self.data.write(to: url, options: options) + return true + } catch { + errorPtr?.pointee = error as NSError + return false + } + } } /// The hash value for the data. diff --git a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift index c53d64eba4d..02f4187acd4 100644 --- a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift +++ b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift @@ -110,7 +110,12 @@ class SentryDataWrapperTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + assertDataSpan( + span, + path: fixture.filePath, + operation: SentrySpanOperationFileRead, + size: fixture.data.count + ) } func testWriteAtomically_CheckTransaction_DebugImages() { @@ -169,7 +174,7 @@ class SentryDataWrapperTests: XCTestCase { } self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: self.fixture.data.count, mainThread: false) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, mainThread: false) expect.fulfill() } @@ -189,7 +194,12 @@ class SentryDataWrapperTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 3) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + assertDataSpan( + span, + path: fixture.filePath, + operation: SentrySpanOperationFileWrite, + size: fixture.data.count + ) } func testDontTrackSentryFilesWrites() { @@ -223,7 +233,12 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(usedPath, fixture.filePath) XCTAssertEqual(data?.count, fixture.data.count) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan( + span, + path: fixture.filePath, + operation: SentrySpanOperationFileRead, + size: fixture.data.count + ) } func testReadFromStringOptionsError() { @@ -244,7 +259,7 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) } func testReadFromURLOptionsError() { @@ -266,7 +281,7 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: url.path, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count) } func testDontTrackSentryFilesRead() { @@ -312,7 +327,7 @@ class SentryDataWrapperTests: XCTestCase { let lastComponent = (path as NSString).lastPathComponent - if operation == SENTRY_FILE_READ_OPERATION { + if operation == SentrySpanOperationFileRead { XCTAssertEqual(span?.spanDescription, lastComponent) } else { let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index 1d73b809446..1b008e05caf 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -3,6 +3,7 @@ #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" +#import "SentrySpanOperations.h" #import "SentrySwizzle.h" #import "SentryTracer.h" #import @@ -67,7 +68,7 @@ - (void)tearDown - (void)test_dataWithContentsOfFile { - [self assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[NSData dataWithContentsOfFile:self->filePath]]; @@ -77,7 +78,7 @@ - (void)test_dataWithContentsOfFile - (void)test_dataWithContentsOfFileOptionsError { [self - assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[NSData @@ -90,7 +91,7 @@ - (void)test_dataWithContentsOfFileOptionsError - (void)test_dataWithContentsOfURL { [self - assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[NSData dataWithContentsOfURL:self->fileUrl]]; }]; @@ -99,7 +100,7 @@ - (void)test_dataWithContentsOfURL - (void)test_dataWithContentsOfURLOptionsError { [self - assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[NSData dataWithContentsOfURL:self->fileUrl @@ -110,7 +111,7 @@ - (void)test_dataWithContentsOfURLOptionsError - (void)test_initWithContentsOfURL { - [self assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[[NSData alloc] initWithContentsOfURL:self->fileUrl]]; @@ -119,7 +120,7 @@ - (void)test_initWithContentsOfURL - (void)test_initWithContentsOfFile { - [self assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[[NSData alloc] initWithContentsOfFile:self->filePath]]; @@ -128,7 +129,7 @@ - (void)test_initWithContentsOfFile - (void)test_writeToFileAtomically { - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite block:^{ [self->someData writeToFile:self->filePath atomically:true]; }]; @@ -137,7 +138,7 @@ - (void)test_writeToFileAtomically - (void)test_writeToUrlAtomically { - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite block:^{ [self->someData writeToURL:self->fileUrl atomically:true]; }]; @@ -146,7 +147,7 @@ - (void)test_writeToUrlAtomically - (void)test_writeToFileOptionsError { - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite block:^{ [self->someData writeToFile:self->filePath options:NSDataWritingAtomic @@ -157,7 +158,7 @@ - (void)test_writeToFileOptionsError - (void)test_writeToUrlOptionsError { - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite block:^{ [self->someData writeToURL:self->fileUrl options:NSDataWritingAtomic @@ -168,7 +169,7 @@ - (void)test_writeToUrlOptionsError - (void)test_NSFileManagerContentAtPath { - [self assertTransactionForOperation:SENTRY_FILE_READ_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileRead block:^{ [self assertData:[NSFileManager.defaultManager contentsAtPath:self->filePath]]; @@ -182,7 +183,7 @@ - (void)test_NSFileManagerCreateFile "disable this test until we fix file IO tracking: " "https://github.com/getsentry/sentry-cocoa/issues/4546"); } - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath contents:self->someData @@ -221,7 +222,7 @@ - (void)assertTransactionForOperation:(NSString *)operation block:(void (^)(void NSString *filename = filePath.lastPathComponent; - if ([operation isEqualToString:SENTRY_FILE_READ_OPERATION]) { + if ([operation isEqualToString:SentrySpanOperationFileRead]) { XCTAssertEqualObjects(ioSpan.spanDescription, filename); } else { NSString *expectedString = [NSString stringWithFormat:@"%@ (%@)", filename, diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift index 9b0324ecc74..adfe94369fd 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSDataTrackerTests.swift @@ -46,8 +46,11 @@ class SentryNSDataTrackerTests: XCTestCase { func testConstants() { //A test to ensure this constants don't accidentally change - XCTAssertEqual("file.read", SENTRY_FILE_READ_OPERATION) - XCTAssertEqual("file.write", SENTRY_FILE_WRITE_OPERATION) + XCTAssertEqual("file.read", SentrySpanOperationFileRead) + XCTAssertEqual( + "file.write", + SentrySpanOperationFileWrite + ) } func testWritePathAtomically() { @@ -116,7 +119,7 @@ class SentryNSDataTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count) } func testWriteAtomically_CheckTransaction_DebugImages() { @@ -175,7 +178,7 @@ class SentryNSDataTrackerTests: XCTestCase { } self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: self.fixture.data.count, mainThread: false) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, mainThread: false) expect.fulfill() } @@ -195,7 +198,7 @@ class SentryNSDataTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 3) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count) } func testDontTrackSentryFilesWrites() { @@ -229,7 +232,7 @@ class SentryNSDataTrackerTests: XCTestCase { XCTAssertEqual(usedPath, fixture.filePath) XCTAssertEqual(data?.count, fixture.data.count) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) } func testReadFromStringOptionsError() { @@ -250,7 +253,7 @@ class SentryNSDataTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) } func testReadFromURLOptionsError() { @@ -272,7 +275,7 @@ class SentryNSDataTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: url.path, operation: SENTRY_FILE_READ_OPERATION, size: fixture.data.count) + assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count) } func testDontTrackSentryFilesRead() { @@ -316,7 +319,7 @@ class SentryNSDataTrackerTests: XCTestCase { let lastComponent = (path as NSString).lastPathComponent - if operation == SENTRY_FILE_READ_OPERATION { + if operation == SentrySpanOperationFileRead { XCTAssertEqual(span?.spanDescription, lastComponent) } else { let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) From 8a3a0120b9c4479e32651a7755a5391e7dba9553 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 11:42:01 +0100 Subject: [PATCH 15/38] refactor to type extension --- Sentry.xcodeproj/project.pbxproj | 12 +- Sources/Sentry/SentryFileIOTracker.m | 68 +++- Sources/Sentry/SentryNSDataSwizzling.m | 6 + Sources/Sentry/SentryNSFileManagerSwizzling.m | 2 + Sources/Sentry/include/SentryFileIOTracker.h | 16 + Sources/Sentry/include/SentryPrivate.h | 2 +- Sources/Sentry/include/SentryTraceOrigins.h | 2 +- Sources/Swift/Helper/Data+SentryTracing.swift | 54 +++ Sources/Swift/Helper/SentryDataWrapper.swift | 358 ------------------ .../Helper/SentryDataWrapperTests.swift | 50 ++- .../IO/SentryFileIOTrackerTests.swift | 43 ++- ...SentryFileIOTrackingIntegrationTests.swift | 10 +- .../IO/SentryNSFileManagerSwizzlingTests.m | 11 +- 13 files changed, 211 insertions(+), 423 deletions(-) create mode 100644 Sources/Swift/Helper/Data+SentryTracing.swift delete mode 100644 Sources/Swift/Helper/SentryDataWrapper.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 4791b46e302..7c1b03f04b1 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,11 +789,11 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; - D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */; }; - D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; + D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */; }; + D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -1878,11 +1878,11 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; - D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileOperations.m; sourceTree = ""; }; - D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapper.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; + D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileOperations.m; sourceTree = ""; }; + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; @@ -2138,7 +2138,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( - D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */, + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */, @@ -4821,7 +4821,7 @@ 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */, 7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */, 7D65260E237F649E00113EA2 /* SentryScope.m in Sources */, - D4EDF9842D0B2A210071E7B3 /* SentryDataWrapper.swift in Sources */, + D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */, 84281C472A57905700EE88F2 /* SentrySample.m in Sources */, 84AC61D329F7541E009EEF61 /* SentryDispatchSourceWrapper.m in Sources */, 62A456E52B0370E0003F19A1 /* SentryUIEventTrackerTransactionMode.m in Sources */, diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 13e002b2e97..baaf04ba74a 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -17,7 +17,6 @@ #import "SentryStacktrace.h" #import "SentryThread.h" #import "SentryThreadInspector.h" -#import "SentryTraceOrigins.h" #import "SentryTracer.h" @interface SentryFileIOTracker () @@ -35,7 +34,7 @@ @implementation SentryFileIOTracker + (instancetype)sharedInstance { - SentryNSDataTracker *tracker = [[SentryNSDataTracker alloc] + SentryFileIOTracker *tracker = [[SentryFileIOTracker alloc] initWithThreadInspector:SentryDependencyContainer.sharedInstance.threadInspector processInfoWrapper:SentryDependencyContainer.sharedInstance.processInfoWrapper]; return tracker; @@ -68,9 +67,10 @@ - (void)disable - (BOOL)measureNSData:(NSData *)data writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile + origin:(NSString *)origin method:(BOOL (^)(NSString *, BOOL))method { - id span = [self startTrackingWritingNSData:data filePath:path]; + id span = [self startTrackingWritingNSData:data filePath:path origin:origin]; BOOL result = method(path, useAuxiliaryFile); @@ -83,10 +83,11 @@ - (BOOL)measureNSData:(NSData *)data - (BOOL)measureNSData:(NSData *)data writeToFile:(NSString *)path options:(NSDataWritingOptions)writeOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(BOOL (^)(NSString *, NSDataWritingOptions, NSError **))method { - id span = [self startTrackingWritingNSData:data filePath:path]; + id span = [self startTrackingWritingNSData:data filePath:path origin:origin]; BOOL result = method(path, writeOptionsMask, error); @@ -97,9 +98,35 @@ - (BOOL)measureNSData:(NSData *)data return result; } -- (NSData *)measureNSDataFromFile:(NSString *)path method:(NSData * (^)(NSString *))method +- (BOOL)measureNSData:(NSData *)data + writeToURL:(NSURL *)url + options:(NSDataWritingOptions)writeOptionsMask + origin:(NSString *)origin + error:(NSError **)error + method:(BOOL (^)(NSURL *, NSDataWritingOptions, NSError **))method +{ + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. + if (![url.scheme isEqualToString:NSURLFileScheme]) + return method(url, writeOptionsMask, error); + + id span = [self startTrackingWritingNSData:data filePath:[url path] origin:origin]; + + BOOL result = method(url, writeOptionsMask, error); + + if (span != nil) { + [self finishTrackingNSData:data span:span]; + } + + return result; +} + +- (NSData *)measureNSDataFromFile:(NSString *)path + origin:(NSString *)origin + method:(NSData * (^)(NSString *))method { - id span = [self startTrackingReadingFilePath:path]; + id span = [self startTrackingReadingFilePath:path origin:origin]; NSData *result = method(path); @@ -113,10 +140,11 @@ - (NSData *)measureNSDataFromFile:(NSString *)path method:(NSData * (^)(NSString - (NSData *)measureNSDataFromFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(NSData * (^)(NSString *, NSDataReadingOptions, NSError **))method { - id span = [self startTrackingReadingFilePath:path]; + id span = [self startTrackingReadingFilePath:path origin:origin]; NSData *result = method(path, readOptionsMask, error); @@ -130,6 +158,7 @@ - (NSData *)measureNSDataFromFile:(NSString *)path - (NSData *)measureNSDataFromURL:(NSURL *)url options:(NSDataReadingOptions)readOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(NSData * (^)(NSURL *, NSDataReadingOptions, NSError **))method { @@ -140,7 +169,7 @@ - (NSData *)measureNSDataFromURL:(NSURL *)url if (![url.scheme isEqualToString:NSURLFileScheme]) return method(url, readOptionsMask, error); - id span = [self startTrackingReadingFilePath:url.path]; + id span = [self startTrackingReadingFilePath:url.path origin:origin]; NSData *result = method(url, readOptionsMask, error); @@ -155,11 +184,12 @@ - (NSData *)measureNSDataFromURL:(NSURL *)url - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path data:(NSData *)data attributes:(NSDictionary *)attributes + origin:(NSString *)origin method: (BOOL (^)(NSString *_Nonnull, NSData *_Nonnull, NSDictionary *_Nonnull))method { - id span = [self startTrackingWritingNSData:data filePath:path]; + id span = [self startTrackingWritingNSData:data filePath:path origin:origin]; BOOL result = method(path, data, attributes); @@ -170,6 +200,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path } - (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin operation:(NSString *)operation size:(NSUInteger)size { @@ -188,7 +219,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path ioSpan = [span startChildWithOperation:operation description:[self transactionDescriptionForFile:path fileSize:size]]; - ioSpan.origin = SentryTraceOriginAutoNSData; + ioSpan.origin = origin; }]; if (ioSpan == nil) { @@ -197,7 +228,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path } SENTRY_LOG_DEBUG( - @"SentryNSDataTracker automatically started a new span with description: %@, operation: %@", + @"SentryFileIOTracker automatically started a new span with description: %@, operation: %@", ioSpan.description, operation); [ioSpan setDataValue:path forKey:@"file.path"]; @@ -237,12 +268,17 @@ - (void)mainThreadExtraInfo:(id)span } } -- (nullable id)startTrackingWritingNSData:(NSData *)data filePath:(NSString *)path +- (nullable id)startTrackingWritingNSData:(NSData *)data + filePath:(NSString *)path + origin:(NSString *)origin { - return [self spanForPath:path operation:SentrySpanOperationFileWrite size:data.length]; + return [self spanForPath:path + origin:origin + operation:SentrySpanOperationFileWrite + size:data.length]; } -- (nullable id)startTrackingReadingFilePath:(NSString *)path +- (nullable id)startTrackingReadingFilePath:(NSString *)path origin:(NSString *)origin { // Some iOS versions nest constructors calls. This counter help us avoid create more than one // span for the same operation. @@ -254,7 +290,7 @@ - (void)mainThreadExtraInfo:(id)span if (count) return nil; - return [self spanForPath:path operation:SentrySpanOperationFileRead size:0]; + return [self spanForPath:path origin:origin operation:SentrySpanOperationFileRead size:0]; } - (void)endTrackingFile @@ -278,7 +314,7 @@ - (void)finishTrackingNSData:(NSData *)data span:(id)span [span setDataValue:[NSNumber numberWithUnsignedInteger:data.length] forKey:@"file.size"]; [span finish]; - SENTRY_LOG_DEBUG(@"SentryNSDataTracker automatically finished span %@", span.description); + SENTRY_LOG_DEBUG(@"SentryFileIOTracker automatically finished span %@", span.description); } - (BOOL)ignoreFile:(NSString *)path diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 6ddd52471a1..567437e3709 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -1,6 +1,7 @@ #import "SentryNSDataSwizzling.h" #import "SentryLog.h" #import "SentrySwizzle.h" +#import "SentryTraceOrigins.h" #import @interface SentryNSDataSwizzling () @@ -45,6 +46,7 @@ + (void)swizzle measureNSData:self writeToFile:path atomically:useAuxiliaryFile + origin:SentryTraceOriginAutoNSData method:^BOOL(NSString *_Nonnull filePath, BOOL isAtomically) { return SentrySWCallOriginal(filePath, isAtomically); }]; @@ -60,6 +62,7 @@ + (void)swizzle measureNSData:self writeToFile:path options:writeOptionsMask + origin:SentryTraceOriginAutoNSData error:error method:^BOOL( NSString *filePath, NSDataWritingOptions options, NSError **outError) { @@ -77,6 +80,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path options:options + origin:SentryTraceOriginAutoNSData error:error method:^NSData *(NSString *filePath, NSDataReadingOptions options, NSError **outError) { @@ -91,6 +95,7 @@ + (void)swizzle SentrySWReturnType(NSData *), SentrySWArguments(NSString * path), SentrySWReplacement({ return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path + origin:SentryTraceOriginAutoNSData method:^NSData *( NSString *filePath) { return SentrySWCallOriginal(filePath); }]; }), @@ -105,6 +110,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromURL:url options:options + origin:SentryTraceOriginAutoNSData error:error method:^NSData *(NSURL *fileUrl, NSDataReadingOptions options, NSError **outError) { diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index a55f4883837..c341e41dceb 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -1,6 +1,7 @@ #import "SentryNSFileManagerSwizzling.h" #import "SentryLog.h" #import "SentrySwizzle.h" +#import "SentryTraceOrigins.h" #import @interface SentryNSFileManagerSwizzling () @@ -59,6 +60,7 @@ + (void)swizzle measureNSFileManagerCreateFileAtPath:path data:data attributes:attributes + origin:SentryTraceOriginAutoNSData method:^BOOL(NSString *path, NSData *data, NSDictionary *attributes) { diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index 8bce81fd196..b1fb0889c18 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -24,6 +24,7 @@ SENTRY_NO_INIT - (BOOL)measureNSData:(NSData *)data writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile + origin:(NSString *)origin method:(BOOL (^)(NSString *, BOOL))method; /** @@ -32,13 +33,25 @@ SENTRY_NO_INIT - (BOOL)measureNSData:(NSData *)data writeToFile:(NSString *)path options:(NSDataWritingOptions)writeOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(BOOL (^)(NSString *, NSDataWritingOptions, NSError **))method; +/** + * Measure NSData 'writeToFile:options:error:' method. + */ +- (BOOL)measureNSData:(NSData *)data + writeToURL:(NSURL *)url + options:(NSDataWritingOptions)writeOptionsMask + origin:(NSString *)origin + error:(NSError **)error + method:(BOOL (^)(NSURL *, NSDataWritingOptions, NSError **))method; + /** * Measure NSData 'initWithContentsOfFile:' method. */ - (nullable NSData *)measureNSDataFromFile:(NSString *)path + origin:(NSString *)origin method:(NSData *_Nullable (^)(NSString *))method; /** @@ -46,6 +59,7 @@ SENTRY_NO_INIT */ - (nullable NSData *)measureNSDataFromFile:(NSString *)path options:(NSDataReadingOptions)readOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(NSData *_Nullable (^)( NSString *, NSDataReadingOptions, NSError **))method; @@ -55,6 +69,7 @@ SENTRY_NO_INIT */ - (nullable NSData *)measureNSDataFromURL:(NSURL *)url options:(NSDataReadingOptions)readOptionsMask + origin:(NSString *)origin error:(NSError **)error method:(NSData *_Nullable (^)( NSURL *, NSDataReadingOptions, NSError **))method; @@ -65,6 +80,7 @@ SENTRY_NO_INIT - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path data:(NSData *)data attributes:(NSDictionary *)attributes + origin:(NSString *)origin method:(BOOL (^)(NSString *, NSData *, NSDictionary *))method; diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index a3bfb8adcad..0640194a491 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -13,10 +13,10 @@ #import "SentryClient+Private.h" #import "SentryDateUtil.h" #import "SentryDisplayLinkWrapper.h" +#import "SentryFileIOTracker.h" #import "SentryFileManager.h" #import "SentryLevelHelper.h" #import "SentryLogC.h" -#import "SentryNSDataTracker.h" #import "SentryRandom.h" #import "SentrySDK+Private.h" #import "SentrySdkInfo.h" diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index a95345d494c..58aa1831a69 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -6,7 +6,7 @@ static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker static NSString *const SentryTraceOriginAutoAppStart = @"auto.app.start"; static NSString *const SentryTraceOriginAutoAppStartProfile = @"auto.app.start.profile"; static NSString *const SentryTraceOriginAutoNSData = @"auto.file.ns_data"; -static NSString *const SentryTraceOriginAutoData = @"auto.file.data"; +static NSString *const SentryTraceOriginManualData = @"manual.file.data"; static NSString *const SentryTraceOriginAutoDBCoreData = @"auto.db.core_data"; static NSString *const SentryTraceOriginAutoHttpNSURLSession = @"auto.http.ns_url_session"; static NSString *const SentryTraceOriginAutoUIViewController = @"auto.ui.view_controller"; diff --git a/Sources/Swift/Helper/Data+SentryTracing.swift b/Sources/Swift/Helper/Data+SentryTracing.swift new file mode 100644 index 00000000000..81905e1912c --- /dev/null +++ b/Sources/Swift/Helper/Data+SentryTracing.swift @@ -0,0 +1,54 @@ +@_implementationOnly import _SentryPrivate + +/// A `Data` extension that automatically tracks read and write operations with Sentry. +/// +/// - Note: Methods provided by this extension reflect the same functionality as the original `Data` methods, +/// but they automatically track the operation with Sentry. +@available(iOS 18, macOS 15, tvOS 18, *) +public extension Data { + + /// Initialize a `Data` with the contents of a `URL`, automatically tracking the operation with Sentry. + /// + /// - parameter url: The `URL` to read. + /// - parameter options: Options for the read operation. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if `url` cannot be read. + init(contentsOfUrlWithSentryTracing url: URL, options: Data.ReadingOptions = []) throws { + let tracker = SentryFileIOTracker.sharedInstance() + // `Data` is bridged to `NSData`, therefore we can use `measureNSData` to track the operation. + self = try tracker + .measureNSData( + from: url, + origin: SentryTraceOriginManualData) { url, options, errorPtr in + do { + return try Data(contentsOf: url, options: options) + } catch { + errorPtr?.pointee = error as NSError + return nil + } + } + } + + /// Write the contents of the `Data` to a location, automatically tracking the operation with Sentry. + /// + /// - parameter url: The location to write the data into. + /// - parameter options: Options for writing the data. Default value is `[]`. + /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. + func writeWithSentryTracing(to url: URL, options: Data.WritingOptions = []) throws { + let tracker = SentryFileIOTracker.sharedInstance() + // `Data` is bridged to `NSData`, therefore we can use `measure` to track the operation + try tracker + .measure( + self, + writeTo: url, + options: options, + origin: SentryTraceOriginManualData) { url, options, errorPtr in + do { + try self.write(to: url, options: options) + return true + } catch { + errorPtr?.pointee = error as NSError + return false + } + } + } +} diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift deleted file mode 100644 index 4db61752d6d..00000000000 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ /dev/null @@ -1,358 +0,0 @@ -@_implementationOnly import _SentryPrivate - -/// A drop-in replacement for the standard ``Swift.Data`` but with automatic tracking for file I/O operations. -/// -/// This structure is intended to resemble the same method signatures as of ``Swift.Data``. -@available(iOS 18, macOS 15, tvOS 18, *) -@frozen public struct SentryDataWrapper: Equatable, Hashable, RandomAccessCollection, MutableCollection, RangeReplaceableCollection, MutableDataProtocol, ContiguousBytes, Sendable { - - /// The wrapped data - public private(set) var data: Data - - /// Convenience initializer to wrap an existing `Data` instance. - public init(data: Data) { - self.data = data - } - - /// See `Data.init(bytes:count:)` - public init(bytes: UnsafeRawPointer, count: Int) { - self.data = Data(bytes: bytes, count: count) - } - - /// See `Data.init(buffer:)` - public init(buffer: UnsafeBufferPointer) { - self.data = Data(buffer: buffer) - } - - /// See `Data.init(buffer:)` - public init(buffer: UnsafeMutableBufferPointer) { - self.data = Data(buffer: buffer) - } - - /// See `Data.init(repeating:count:)` - public init(repeating repeatedValue: UInt8, count: Int) { - self.data = Data(repeating: repeatedValue, count: count) - } - - /// See `Data.init(capacity:)` - public init(capacity: Int) { - self.data = Data(capacity: capacity) - } - - /// See `Data.init(count:)` - public init(count: Int) { - self.data = Data(count: count) - } - - /// See `Data.init()` - public init() { - self.data = Data() - } - - /// See `Data.init(bytesNoCopy:count:deallocator:)` - public init(bytesNoCopy bytes: UnsafeMutableRawPointer, count: Int, deallocator: Data.Deallocator) { - self.data = Data(bytesNoCopy: bytes, count: count, deallocator: deallocator) - } - - /// See `Data.init(_:)` - public init(_ elements: S) where S: Sequence, S.Element == UInt8 { - self.data = Data(elements) - } - - /// See `Data.init(bytes:)` - @available(swift 4.2) - @available(swift, deprecated: 5, message: "use `init(_:)` instead") - public init(bytes elements: S) where S: Sequence, S.Element == UInt8 { - self.data = Data(bytes: elements) - } - - /// See `Data.init(contentsOf:options:)` - public init(contentsOf url: URL, options: Data.ReadingOptions = []) throws { - let tracker = SentryNSDataTracker.sharedInstance() - self.data = try tracker.measureNSData( - from: url - ) { url, options, errorPtr in - do { - return try Data(contentsOf: url, options: options) - } catch { - errorPtr?.pointee = error as NSError - return nil - } - } - } - - /// See `Data.reserveCapacity(_:)` - public mutating func reserveCapacity(_ minimumCapacity: Int) { - self.data.reserveCapacity(minimumCapacity) - } - - /// See `Data.count` - public var count: Int { - return self.data.count - } - - /// See `Data.regions` - public var regions: CollectionOfOne { - return self.data.regions - } - - /// See `Data.withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` - @available(swift, deprecated: 5, message: "use `withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` instead") - public func withUnsafeBytes(_ body: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { - return try self.data.withUnsafeBytes(body) - } - - /// See `Data.withUnsafeBytes(_: (UnsafeRawBufferPointer) throws -> R) rethrows -> R` - public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> ResultType) rethrows -> ResultType { - return try self.data.withUnsafeBytes(body) - } - - /// See `Data.withContiguousStorageIfAvailable(_ body: (_ buffer: UnsafeBufferPointer) throws -> ResultType) rethrows -> ResultType?` - public func withContiguousStorageIfAvailable(_ body: (_ buffer: UnsafeBufferPointer) throws -> ResultType) rethrows -> ResultType? { - return try self.data.withContiguousStorageIfAvailable(body) - } - - /// See `Data.withUnsafeMutableBytes(_: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R` - public mutating func withUnsafeMutableBytes(_ body: (UnsafeMutableRawBufferPointer) throws -> ResultType) rethrows -> ResultType { - return try self.data.withUnsafeMutableBytes(body) - } - - /// See `Data.copyBytes(to:count:)` - public func copyBytes(to pointer: UnsafeMutablePointer, count: Int) { - self.data.copyBytes(to: pointer, count: count) - } - - /// See `Data.copyBytes(to:from:)` - public func copyBytes(to pointer: UnsafeMutablePointer, from range: Range) { - self.data.copyBytes(to: pointer, from: range) - } - - /// See `Data.copyBytes(to:from:)` - public func copyBytes(to buffer: UnsafeMutableBufferPointer, from range: Range? = nil) -> Int { - return self.data.copyBytes(to: buffer, from: range) - } - - /// See `Data.enumerateBytes(_:)` - @available(swift, deprecated: 5, message: "use `regions` or `for-in` instead") - public func enumerateBytes(_ block: (_ buffer: UnsafeBufferPointer, _ byteIndex: Data.Index, _ stop: inout Bool) -> Void) { - self.data.enumerateBytes(block) - } - - /// See `Data.append(_:)` - public mutating func append(_ bytes: UnsafePointer, count: Int) { - self.data.append(bytes, count: count) - } - - /// See `Data.append(_:)` - public mutating func append(_ other: Data) { - self.data.append(other) - } - - /// See `Data.append(_ buffer: UnsafeBufferPointer)` - public mutating func append(_ buffer: UnsafeBufferPointer) { - self.data.append(buffer) - } - - /// See `Data.append(contentsOf:)` - public mutating func append(contentsOf bytes: [UInt8]) { - self.data.append(contentsOf: bytes) - } - - /// See `Data.append(contentsOf:)` - public mutating func append(contentsOf elements: S) where S: Sequence, S.Element == UInt8 { - self.data.append(contentsOf: elements) - } - - /// See `Data.resetBytes(in:)` - public mutating func resetBytes(in range: Range) { - self.data.resetBytes(in: range) - } - - /// See `Data.replaceSubrange(_:with:)` - public mutating func replaceSubrange(_ subrange: Range, with data: Data) { - self.data.replaceSubrange(subrange, with: data) - } - - /// See `Data.replaceSubrange(_:with:)` - public mutating func replaceSubrange(_ subrange: Range, with buffer: UnsafeBufferPointer) { - self.data.replaceSubrange(subrange, with: buffer) - } - - /// See `Data.replaceSubrange(_ subrange: Range, with newElements: ByteCollection) where ByteCollection: Collection, ByteCollection.Element == UInt8` - public mutating func replaceSubrange(_ subrange: Range, with newElements: ByteCollection) where ByteCollection: Collection, ByteCollection.Element == UInt8 { - self.data.replaceSubrange(subrange, with: newElements) - } - - /// See `Data.replaceSubrange(_:with:)` - public mutating func replaceSubrange(_ subrange: Range, with bytes: UnsafeRawPointer, count cnt: Int) { - self.data.replaceSubrange(subrange, with: bytes, count: cnt) - } - - /// See `Data.subdata(in:)` - public func subdata(in range: Range) -> Data { - return self.data.subdata(in: range) - } - - /// See `Data.write(to:options:)` - public func write(to url: URL, options: Data.WritingOptions = []) throws { - let tracker = SentryNSDataTracker.sharedInstance() - try tracker.measure(self.data, writeToFile: url.path) { _, options, errorPtr in - do { - try self.data.write(to: url, options: options) - return true - } catch { - errorPtr?.pointee = error as NSError - return false - } - } - } - - /// The hash value for the data. - public func hash(into hasher: inout Hasher) { - self.data.hash(into: &hasher) - } - - /// See `Data.advanced(by:)` - public func advanced(by amount: Int) -> Data { - return self.data.advanced(by: amount) - } - - /// See `Data.subscript(index:)` - public subscript(index: Data.Index) -> UInt8 { - get { - return self.data[index] - } - set { - self.data[index] = newValue - } - } - - /// See `Data.subscript(bounds:)` - public subscript(bounds: Range) -> Data { - get { - return self.data[bounds] - } - set { - self.data[bounds] = newValue - } - } - - /// See `Data.subscript(rangeExpression: R) -> Data where R: RangeExpression, R.Bound: FixedWidthInteger` - public subscript(rangeExpression: R) -> Data where R: RangeExpression, R.Bound: FixedWidthInteger { - get { - self.data[rangeExpression] - } - set { - self.data[rangeExpression] = newValue - } - } - - /// See `Data.startIndex` - public var startIndex: Data.Index { - return self.data.startIndex - } - - /// See `Data.endIndex` - public var endIndex: Data.Index { - return self.data.endIndex - } - - /// See `Data.index(before:)` - public func index(before i: Data.Index) -> Data.Index { - return self.data.index(before: i) - } - - /// See `Data.index(after:)` - public func index(after i: Data.Index) -> Data.Index { - return self.data.index(after: i) - } - - /// See `Data.indices` - public var indices: Range { - return self.data.indices - } - - /// See `Data.makeIterator()` - public func makeIterator() -> Data.Iterator { - return self.data.makeIterator() - } - - /// See `Data.range(of:options:in:)` - public func range(of dataToFind: Data, options: Data.SearchOptions = [], in range: Range? = nil) -> Range? { - return self.data.range(of: dataToFind, options: options, in: range) - } - - /// See `Data.==` - public static func == (d1: SentryDataWrapper, d2: SentryDataWrapper) -> Bool { - return d1.data == d2.data - } - - /// See `Data.hashValue` - public var hashValue: Int { - return self.data.hashValue - } - - /// See `Data.init?(base64Encoded:options:)` - public init?(base64Encoded base64String: String, options: Data.Base64DecodingOptions = []) { - guard let data = Data(base64Encoded: base64String, options: options) else { - return nil - } - self.data = data - } - - /// See `Data.init?(base64Encoded:options:)` - public init?(base64Encoded base64Data: Data, options: Data.Base64DecodingOptions = []) { - guard let data = Data(base64Encoded: base64Data, options: options) else { - return nil - } - self.data = data - } - - /// See `Data.base64EncodedString(options:)` - public func base64EncodedString(options: Data.Base64EncodingOptions = []) -> String { - return self.data.base64EncodedString(options: options) - } - - /// See `Data.base64EncodedData(options:)` - public func base64EncodedData(options: Data.Base64EncodingOptions = []) -> Data { - return self.data.base64EncodedData(options: options) - } - - /// See `Data.init(referencing:)` - public init(referencing reference: NSData) { - self.data = Data(referencing: reference) - } -} - -@available(iOS 18, macOS 15, tvOS 18, *) -extension SentryDataWrapper: CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable { - - /// See `Data.description` - public var description: String { - "SentryDataWrapper(\(self.data.description))" - } - - /// See `Data.debugDescription` - public var debugDescription: String { - "SentryDataWrapper(\(self.data.debugDescription))" - } - - /// See `Data.customMirror` - public var customMirror: Mirror { - self.data.customMirror - } -} - -@available(iOS 18, macOS 15, tvOS 18, *) -extension SentryDataWrapper: Codable { - - /// See `Data.init(from:)` - public init(from decoder: any Decoder) throws { - self.data = try Data(from: decoder) - } - - /// See `Data.encode(to:)` - public func encode(to encoder: any Encoder) throws { - try self.data.encode(to: encoder) - } -} diff --git a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift index 02f4187acd4..defd66fec03 100644 --- a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift +++ b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift @@ -12,7 +12,7 @@ class SentryDataWrapperTests: XCTestCase { let threadInspector = TestThreadInspector.instance let imageProvider = TestDebugImageProvider() - func getSut() -> SentryNSDataTracker { + func getSut() -> SentryFileIOTracker { imageProvider.debugImages = [TestData.debugImage] SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider @@ -21,7 +21,7 @@ class SentryDataWrapperTests: XCTestCase { let processInfoWrapper = TestSentryNSProcessInfoWrapper() processInfoWrapper.overrides.processDirectoryPath = "sentrytest" - let result = SentryNSDataTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) + let result = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) SentryDependencyContainer.sharedInstance().dateProvider = dateProvider result.enable() return result @@ -49,7 +49,12 @@ class SentryDataWrapperTests: XCTestCase { var methodPath: String? var methodAuxiliareFile: Bool? - var result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { path, useAuxiliareFile -> Bool in + var result = sut.measure( + fixture.data, + writeToFile: fixture.filePath, + atomically: false, + origin: SentryTraceOriginAutoNSData + ) { path, useAuxiliareFile -> Bool in methodPath = path methodAuxiliareFile = useAuxiliareFile return false @@ -59,7 +64,8 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertFalse(methodAuxiliareFile!) XCTAssertFalse(result) - result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true) { _, useAuxiliareFile -> Bool in + result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, + origin: SentryTraceOriginAutoNSData) { _, useAuxiliareFile -> Bool in methodAuxiliareFile = useAuxiliareFile return true } @@ -74,7 +80,8 @@ class SentryDataWrapperTests: XCTestCase { var methodOptions: NSData.WritingOptions? var methodError: NSError? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { path, writingOption, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, + origin: SentryTraceOriginAutoNSData) { path, writingOption, _ -> Bool in methodPath = path methodOptions = writingOption return true @@ -84,7 +91,8 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(methodOptions, .atomic) do { - try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting) { _, writingOption, errorPointer -> Bool in + try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, + origin: SentryTraceOriginAutoNSData) { _, writingOption, errorPointer -> Bool in methodOptions = writingOption errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) return false @@ -102,7 +110,8 @@ class SentryDataWrapperTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, + origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -123,7 +132,8 @@ class SentryDataWrapperTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, + origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -148,7 +158,8 @@ class SentryDataWrapperTests: XCTestCase { var span: SentrySpan? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, + origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) as? SentrySpan XCTAssertFalse(span?.isFinished ?? true) return true @@ -166,7 +177,8 @@ class SentryDataWrapperTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, + origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -186,7 +198,8 @@ class SentryDataWrapperTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, + origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 3) @@ -208,7 +221,8 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? let expect = expectation(description: "") - try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, + origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in span = self.firstSpan(transaction) expect.fulfill() return true @@ -224,7 +238,8 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? var usedPath: String? - let data = sut.measureNSData(fromFile: fixture.filePath) { path in + let data = sut.measureNSData(fromFile: fixture.filePath, + origin: SentryTraceOriginAutoNSData) { path in span = self.firstSpan(transaction) usedPath = path return self.fixture.data @@ -248,7 +263,8 @@ class SentryDataWrapperTests: XCTestCase { var usedPath: String? var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached) { path, options, _ -> Data in + let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, + origin: SentryTraceOriginAutoNSData) { path, options, _ -> Data in span = self.firstSpan(transaction) usedOptions = options usedPath = path @@ -270,7 +286,8 @@ class SentryDataWrapperTests: XCTestCase { let url = URL(fileURLWithPath: fixture.filePath) var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(from: url, options: .uncached) { url, options, _ in + let data = try? sut.measureNSData(from: url, options: .uncached, + origin: SentryTraceOriginAutoNSData) { url, options, _ in span = self.firstSpan(transaction) usedOptions = options usedUrl = url @@ -290,7 +307,8 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? let expect = expectation(description: "") - let _ = sut.measureNSData(fromFile: fixture.sentryPath) { _ in + let _ = sut.measureNSData(fromFile: fixture.sentryPath, + origin: SentryTraceOriginAutoNSData) { _ in span = self.firstSpan(transaction) expect.fulfill() return nil diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index b711c370151..c1f4b7a1e8b 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -1,3 +1,4 @@ +@testable import Sentry import SentryTestUtils import XCTest @@ -58,7 +59,12 @@ class SentryFileIOTrackerTests: XCTestCase { var methodPath: String? var methodAuxiliareFile: Bool? - var result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { path, useAuxiliareFile -> Bool in + var result = sut.measure( + fixture.data, + writeToFile: fixture.filePath, + atomically: false, + origin: SentryTraceOriginAutoNSData + ) { path, useAuxiliareFile -> Bool in methodPath = path methodAuxiliareFile = useAuxiliareFile return false @@ -68,7 +74,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertFalse(methodAuxiliareFile!) XCTAssertFalse(result) - result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true) { _, useAuxiliareFile -> Bool in + result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, origin: SentryTraceOriginAutoNSData) { _, useAuxiliareFile -> Bool in methodAuxiliareFile = useAuxiliareFile return true } @@ -83,7 +89,7 @@ class SentryFileIOTrackerTests: XCTestCase { var methodOptions: NSData.WritingOptions? var methodError: NSError? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { path, writingOption, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: SentryTraceOriginAutoNSData) { path, writingOption, _ -> Bool in methodPath = path methodOptions = writingOption return true @@ -93,7 +99,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(methodOptions, .atomic) do { - try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting) { _, writingOption, errorPointer -> Bool in + try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, origin: SentryTraceOriginAutoNSData) { _, writingOption, errorPointer -> Bool in methodOptions = writingOption errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) return false @@ -111,7 +117,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -127,7 +133,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -152,7 +158,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: SentrySpan? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) as? SentrySpan XCTAssertFalse(span?.isFinished ?? true) return true @@ -170,7 +176,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false) { _, _ -> Bool in + sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -190,7 +196,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 3) @@ -207,7 +213,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? let expect = expectation(description: "") - try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in span = self.firstSpan(transaction) expect.fulfill() return true @@ -223,7 +229,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? var usedPath: String? - let data = sut.measureNSData(fromFile: fixture.filePath) { path in + let data = sut.measureNSData(fromFile: fixture.filePath, origin: SentryTraceOriginAutoNSData) { path in span = self.firstSpan(transaction) usedPath = path return self.fixture.data @@ -242,7 +248,7 @@ class SentryFileIOTrackerTests: XCTestCase { var usedPath: String? var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached) { path, options, _ -> Data in + let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, origin: SentryTraceOriginAutoNSData) { path, options, _ -> Data in span = self.firstSpan(transaction) usedOptions = options usedPath = path @@ -264,7 +270,7 @@ class SentryFileIOTrackerTests: XCTestCase { let url = URL(fileURLWithPath: fixture.filePath) var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(from: url, options: .uncached) { url, options, _ in + let data = try? sut.measureNSData(from: url, options: .uncached, origin: SentryTraceOriginAutoNSData) { url, options, _ in span = self.firstSpan(transaction) usedOptions = options usedUrl = url @@ -290,7 +296,7 @@ class SentryFileIOTrackerTests: XCTestCase { sut.measureNSFileManagerCreateFile(atPath: fixture.filePath, data: fixture.data, attributes: [ FileAttributeKey.size: 123 - ], method: { path, data, attributes in + ], origin: SentryTraceOriginAutoNSData, method: { path, data, attributes in methodPath = path methodData = data methodAttributes = attributes @@ -306,7 +312,12 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(methodAttributes?[FileAttributeKey.size] as? Int, 123) assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan(span, path: fixture.filePath, operation: SENTRY_FILE_WRITE_OPERATION, size: fixture.data.count) + assertDataSpan( + span, + path: fixture.filePath, + operation: SentrySpanOperationFileWrite, + size: fixture.data.count + ) } func testDontTrackSentryFilesRead() { @@ -315,7 +326,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? let expect = expectation(description: "") - let _ = sut.measureNSData(fromFile: fixture.sentryPath) { _ in + let _ = sut.measureNSData(fromFile: fixture.sentryPath, origin: SentryTraceOriginAutoNSData) { _ in span = self.firstSpan(transaction) expect.fulfill() return nil diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index ef6cef4a8ac..0bb133cf95c 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -118,7 +118,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { - try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL) + try? fixture.data.writeWithSentryTracing(to: fixture.fileURL) } } @@ -130,7 +130,8 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { - try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL, options: .atomic) + try? fixture.data + .writeWithSentryTracing(to: fixture.fileURL, options: .atomic) } } @@ -199,7 +200,8 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { - let _ = try? SentryDataWrapper(contentsOf: fixture.fileURL) + let data = try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL) + XCTAssertEqual(data?.count, fixture.data.count) } } @@ -211,7 +213,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { - let data = try? SentryDataWrapper(contentsOf: fixture.fileURL, options: .uncached) + let data = try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL, options: .uncached) XCTAssertEqual(data?.count, fixture.data.count) } } diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index 7dc199e3eaf..f8084a1085a 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -4,6 +4,7 @@ #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" +#import "SentrySpanOperations.h" #import "SentrySwizzle.h" #import "SentryThreadInspector.h" #import "SentryTracer.h" @@ -82,7 +83,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabl XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -98,7 +99,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -117,7 +118,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -135,7 +136,7 @@ - (void)testNSFileManagerCreateFile_iOS18macOS15tvOS18OrLater_experimentalFlagEn XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; - [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + [self assertTransactionForOperation:SentrySpanOperationFileWrite spanCount:1 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -178,7 +179,7 @@ - (void)assertTransactionForOperation:(NSString *)operation NSString *filename = filePath.lastPathComponent; - if ([operation isEqualToString:SENTRY_FILE_READ_OPERATION]) { + if ([operation isEqualToString:SentrySpanOperationFileRead]) { XCTAssertEqualObjects(ioSpan.spanDescription, filename); } else { NSString *expectedString = [NSString stringWithFormat:@"%@ (%@)", filename, From b03c19a8e2cf7d3ceb4108cb423013dcd70d4f2c Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 12:04:40 +0100 Subject: [PATCH 16/38] add trace origin to swift file --- Sentry.xcodeproj/project.pbxproj | 12 +++ Sources/Sentry/include/SentryTraceOrigins.h | 1 - Sources/Swift/Helper/Data+SentryTracing.swift | 4 +- .../Transactions/SentryTraceOrigins.swift | 6 ++ .../Helper/SentryDataWrapperTests.swift | 72 ++++++++-------- .../IO/SentryFileIOTrackerTests.swift | 86 +++++++++++-------- ...SentryFileIOTrackingIntegrationTests.swift | 39 +++++---- 7 files changed, 129 insertions(+), 91 deletions(-) create mode 100644 Sources/Swift/Transactions/SentryTraceOrigins.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 7c1b03f04b1..a3fc09cf174 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,6 +789,7 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; + D48724DB2D352597005DE483 /* SentryTraceOrigins.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; @@ -1878,6 +1879,7 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; + D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigins.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; @@ -3660,6 +3662,14 @@ name = Transaction; sourceTree = ""; }; + D48724D92D35258A005DE483 /* Transactions */ = { + isa = PBXGroup; + children = ( + D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */, + ); + path = Transactions; + sourceTree = ""; + }; D4F2B5332D0C69CC00649E42 /* Recording */ = { isa = PBXGroup; children = ( @@ -3671,6 +3681,7 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + D48724D92D35258A005DE483 /* Transactions */, D8CAC02D2BA0663E00E38F34 /* Integrations */, 621D9F2D2B9B030E003D94DE /* Helper */, D8F016B42B962533007B9AFB /* Extensions */, @@ -4899,6 +4910,7 @@ 7BC9A20628F41781001E7C4C /* SentryMeasurementUnit.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, 63FE713520DA4C1100CDBAE8 /* SentryCrashMemory.c in Sources */, + D48724DB2D352597005DE483 /* SentryTraceOrigins.swift in Sources */, 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryAsyncSafeLog.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 58aa1831a69..7922cdb9a7c 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -6,7 +6,6 @@ static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker static NSString *const SentryTraceOriginAutoAppStart = @"auto.app.start"; static NSString *const SentryTraceOriginAutoAppStartProfile = @"auto.app.start.profile"; static NSString *const SentryTraceOriginAutoNSData = @"auto.file.ns_data"; -static NSString *const SentryTraceOriginManualData = @"manual.file.data"; static NSString *const SentryTraceOriginAutoDBCoreData = @"auto.db.core_data"; static NSString *const SentryTraceOriginAutoHttpNSURLSession = @"auto.http.ns_url_session"; static NSString *const SentryTraceOriginAutoUIViewController = @"auto.ui.view_controller"; diff --git a/Sources/Swift/Helper/Data+SentryTracing.swift b/Sources/Swift/Helper/Data+SentryTracing.swift index 81905e1912c..aa37e7b2c4f 100644 --- a/Sources/Swift/Helper/Data+SentryTracing.swift +++ b/Sources/Swift/Helper/Data+SentryTracing.swift @@ -18,7 +18,7 @@ public extension Data { self = try tracker .measureNSData( from: url, - origin: SentryTraceOriginManualData) { url, options, errorPtr in + origin: SentryTraceOrigin.manualData) { url, options, errorPtr in do { return try Data(contentsOf: url, options: options) } catch { @@ -41,7 +41,7 @@ public extension Data { self, writeTo: url, options: options, - origin: SentryTraceOriginManualData) { url, options, errorPtr in + origin: SentryTraceOrigin.manualData) { url, options, errorPtr in do { try self.write(to: url, options: options) return true diff --git a/Sources/Swift/Transactions/SentryTraceOrigins.swift b/Sources/Swift/Transactions/SentryTraceOrigins.swift new file mode 100644 index 00000000000..d04c9d799b8 --- /dev/null +++ b/Sources/Swift/Transactions/SentryTraceOrigins.swift @@ -0,0 +1,6 @@ +// This file is the Swift addition for `SentryTraceOrigins.h` in the `Sentry` module. + +@objcMembers +public class SentryTraceOrigin { + static let manualData = "manual.file.data" +} diff --git a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift index defd66fec03..4b8f245d0c2 100644 --- a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift +++ b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift @@ -53,7 +53,7 @@ class SentryDataWrapperTests: XCTestCase { fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData + origin: "custom.origin" ) { path, useAuxiliareFile -> Bool in methodPath = path methodAuxiliareFile = useAuxiliareFile @@ -65,7 +65,7 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertFalse(result) result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, - origin: SentryTraceOriginAutoNSData) { _, useAuxiliareFile -> Bool in + origin: "custom.origin") { _, useAuxiliareFile -> Bool in methodAuxiliareFile = useAuxiliareFile return true } @@ -81,7 +81,7 @@ class SentryDataWrapperTests: XCTestCase { var methodError: NSError? try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, - origin: SentryTraceOriginAutoNSData) { path, writingOption, _ -> Bool in + origin: "custom.origin") { path, writingOption, _ -> Bool in methodPath = path methodOptions = writingOption return true @@ -92,7 +92,7 @@ class SentryDataWrapperTests: XCTestCase { do { try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, - origin: SentryTraceOriginAutoNSData) { _, writingOption, errorPointer -> Bool in + origin: "custom.origin") { _, writingOption, errorPointer -> Bool in methodOptions = writingOption errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) return false @@ -111,7 +111,7 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -122,8 +122,8 @@ class SentryDataWrapperTests: XCTestCase { assertDataSpan( span, path: fixture.filePath, - operation: SentrySpanOperationFileRead, - size: fixture.data.count + operation: SentrySpanOperationFileWrite, + size: fixture.data.count, origin: "custom.origin" ) } @@ -133,7 +133,7 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -159,7 +159,7 @@ class SentryDataWrapperTests: XCTestCase { var span: SentrySpan? sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) as? SentrySpan XCTAssertFalse(span?.isFinished ?? true) return true @@ -178,7 +178,7 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -186,7 +186,7 @@ class SentryDataWrapperTests: XCTestCase { } self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, mainThread: false) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, origin: "custom.origin", mainThread: false) expect.fulfill() } @@ -199,7 +199,7 @@ class SentryDataWrapperTests: XCTestCase { var span: Span? try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, - origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in + origin: "custom.origin") { _, _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 3) @@ -211,7 +211,7 @@ class SentryDataWrapperTests: XCTestCase { span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, - size: fixture.data.count + size: fixture.data.count, origin: "custom.origin" ) } @@ -222,7 +222,7 @@ class SentryDataWrapperTests: XCTestCase { let expect = expectation(description: "") try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, - origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in + origin: "custom.origin") { _, _, _ -> Bool in span = self.firstSpan(transaction) expect.fulfill() return true @@ -239,7 +239,7 @@ class SentryDataWrapperTests: XCTestCase { var usedPath: String? let data = sut.measureNSData(fromFile: fixture.filePath, - origin: SentryTraceOriginAutoNSData) { path in + origin: "custom.origin") { path in span = self.firstSpan(transaction) usedPath = path return self.fixture.data @@ -252,7 +252,7 @@ class SentryDataWrapperTests: XCTestCase { span, path: fixture.filePath, operation: SentrySpanOperationFileRead, - size: fixture.data.count + size: fixture.data.count, origin: "custom.origin" ) } @@ -264,7 +264,7 @@ class SentryDataWrapperTests: XCTestCase { var usedOptions: NSData.ReadingOptions? let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, - origin: SentryTraceOriginAutoNSData) { path, options, _ -> Data in + origin: "custom.origin") { path, options, _ -> Data in span = self.firstSpan(transaction) usedOptions = options usedPath = path @@ -275,7 +275,7 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") } func testReadFromURLOptionsError() { @@ -287,7 +287,7 @@ class SentryDataWrapperTests: XCTestCase { var usedOptions: NSData.ReadingOptions? let data = try? sut.measureNSData(from: url, options: .uncached, - origin: SentryTraceOriginAutoNSData) { url, options, _ in + origin: "custom.origin") { url, options, _ in span = self.firstSpan(transaction) usedOptions = options usedUrl = url @@ -298,7 +298,7 @@ class SentryDataWrapperTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count) + assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") } func testDontTrackSentryFilesRead() { @@ -308,7 +308,7 @@ class SentryDataWrapperTests: XCTestCase { let expect = expectation(description: "") let _ = sut.measureNSData(fromFile: fixture.sentryPath, - origin: SentryTraceOriginAutoNSData) { _ in + origin: "custom.origin") { _ in span = self.firstSpan(transaction) expect.fulfill() return nil @@ -325,37 +325,37 @@ class SentryDataWrapperTests: XCTestCase { return result?.first } - private func assertDataSpan(_ span: Span?, path: String, operation: String, size: Int, mainThread: Bool = true ) { - XCTAssertNotNil(span) - XCTAssertEqual(span?.operation, operation) - XCTAssertEqual(span?.origin, "auto.file.ns_data") - XCTAssertTrue(span?.isFinished ?? false) - XCTAssertEqual(span?.data["file.size"] as? Int, size) - XCTAssertEqual(span?.data["file.path"] as? String, path) - XCTAssertEqual(span?.data["blocked_main_thread"] as? Bool ?? false, mainThread) + private func assertDataSpan(_ span: Span?, path: String, operation: String, size: Int, origin: String, mainThread: Bool = true, file: StaticString = #file, line: UInt = #line) { + XCTAssertNotNil(span, file: file, line: line) + XCTAssertEqual(span?.operation, operation, file: file, line: line) + XCTAssertEqual(span?.origin, origin, file: file, line: line) + XCTAssertTrue(span?.isFinished ?? false, file: file, line: line) + XCTAssertEqual(span?.data["file.size"] as? Int, size, file: file, line: line) + XCTAssertEqual(span?.data["file.path"] as? String, path, file: file, line: line) + XCTAssertEqual(span?.data["blocked_main_thread"] as? Bool ?? false, mainThread, file: file, line: line) if mainThread { guard let frames = (span as? SentrySpan)?.frames else { - XCTFail("File IO Span in the main thread has no frames") + XCTFail("File IO Span in the main thread has no frames", file: file, line: line) return } - XCTAssertEqual(frames.first, TestData.mainFrame) - XCTAssertEqual(frames.last, TestData.testFrame) + XCTAssertEqual(frames.first, TestData.mainFrame, file: file, line: line) + XCTAssertEqual(frames.last, TestData.testFrame, file: file, line: line) } let lastComponent = (path as NSString).lastPathComponent if operation == SentrySpanOperationFileRead { - XCTAssertEqual(span?.spanDescription, lastComponent) + XCTAssertEqual(span?.spanDescription, lastComponent, file: file, line: line) } else { let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) - XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))") + XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))", file: file, line: line) } } - private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval) { + private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval, file: StaticString = #file, line: UInt = #line) { let duration = span?.timestamp?.timeIntervalSince(span!.startTimestamp!) - XCTAssertEqual(duration, expectedDuration) + XCTAssertEqual(duration, expectedDuration, file: file, line: line) } private func advanceTime(bySeconds: TimeInterval) { diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index c1f4b7a1e8b..1890995a02e 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -63,7 +63,7 @@ class SentryFileIOTrackerTests: XCTestCase { fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: SentryTraceOriginAutoNSData + origin: "custom.origin" ) { path, useAuxiliareFile -> Bool in methodPath = path methodAuxiliareFile = useAuxiliareFile @@ -74,7 +74,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertFalse(methodAuxiliareFile!) XCTAssertFalse(result) - result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, origin: SentryTraceOriginAutoNSData) { _, useAuxiliareFile -> Bool in + result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, origin: "custom.origin") { _, useAuxiliareFile -> Bool in methodAuxiliareFile = useAuxiliareFile return true } @@ -89,7 +89,7 @@ class SentryFileIOTrackerTests: XCTestCase { var methodOptions: NSData.WritingOptions? var methodError: NSError? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: SentryTraceOriginAutoNSData) { path, writingOption, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: "custom.origin") { path, writingOption, _ -> Bool in methodPath = path methodOptions = writingOption return true @@ -99,7 +99,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(methodOptions, .atomic) do { - try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, origin: SentryTraceOriginAutoNSData) { _, writingOption, errorPointer -> Bool in + try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, origin: "custom.origin") { _, writingOption, errorPointer -> Bool in methodOptions = writingOption errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) return false @@ -117,7 +117,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -125,7 +125,7 @@ class SentryFileIOTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count, origin: "custom.origin") } func testWriteAtomically_CheckTransaction_DebugImages() { @@ -133,7 +133,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -158,7 +158,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: SentrySpan? - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) as? SentrySpan XCTAssertFalse(span?.isFinished ?? true) return true @@ -176,7 +176,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, origin: SentryTraceOriginAutoNSData) { _, _ -> Bool in + sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, origin: "custom.origin") { _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 4) @@ -184,7 +184,7 @@ class SentryFileIOTrackerTests: XCTestCase { } self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, mainThread: false) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, origin: "custom.origin", mainThread: false) expect.fulfill() } @@ -196,7 +196,7 @@ class SentryFileIOTrackerTests: XCTestCase { let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) var span: Span? - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, origin: "custom.origin") { _, _, _ -> Bool in span = self.firstSpan(transaction) XCTAssertFalse(span?.isFinished ?? true) self.advanceTime(bySeconds: 3) @@ -204,7 +204,7 @@ class SentryFileIOTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 3) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count, origin: "custom.origin") } func testDontTrackSentryFilesWrites() { @@ -213,7 +213,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? let expect = expectation(description: "") - try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, origin: SentryTraceOriginAutoNSData) { _, _, _ -> Bool in + try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, origin: "custom.origin") { _, _, _ -> Bool in span = self.firstSpan(transaction) expect.fulfill() return true @@ -229,7 +229,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? var usedPath: String? - let data = sut.measureNSData(fromFile: fixture.filePath, origin: SentryTraceOriginAutoNSData) { path in + let data = sut.measureNSData(fromFile: fixture.filePath, origin: "custom.origin") { path in span = self.firstSpan(transaction) usedPath = path return self.fixture.data @@ -238,7 +238,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(usedPath, fixture.filePath) XCTAssertEqual(data?.count, fixture.data.count) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") } func testReadFromStringOptionsError() { @@ -248,7 +248,7 @@ class SentryFileIOTrackerTests: XCTestCase { var usedPath: String? var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, origin: SentryTraceOriginAutoNSData) { path, options, _ -> Data in + let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, origin: "custom.origin") { path, options, _ -> Data in span = self.firstSpan(transaction) usedOptions = options usedPath = path @@ -259,7 +259,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count) + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") } func testReadFromURLOptionsError() { @@ -270,7 +270,7 @@ class SentryFileIOTrackerTests: XCTestCase { let url = URL(fileURLWithPath: fixture.filePath) var usedOptions: NSData.ReadingOptions? - let data = try? sut.measureNSData(from: url, options: .uncached, origin: SentryTraceOriginAutoNSData) { url, options, _ in + let data = try? sut.measureNSData(from: url, options: .uncached, origin: "custom.origin") { url, options, _ in span = self.firstSpan(transaction) usedOptions = options usedUrl = url @@ -281,7 +281,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count) + assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") } func testCreateFile() { @@ -296,7 +296,7 @@ class SentryFileIOTrackerTests: XCTestCase { sut.measureNSFileManagerCreateFile(atPath: fixture.filePath, data: fixture.data, attributes: [ FileAttributeKey.size: 123 - ], origin: SentryTraceOriginAutoNSData, method: { path, data, attributes in + ], origin: "custom.origin", method: { path, data, attributes in methodPath = path methodData = data methodAttributes = attributes @@ -316,7 +316,7 @@ class SentryFileIOTrackerTests: XCTestCase { span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, - size: fixture.data.count + size: fixture.data.count, origin: "custom.origin" ) } @@ -326,7 +326,7 @@ class SentryFileIOTrackerTests: XCTestCase { var span: Span? let expect = expectation(description: "") - let _ = sut.measureNSData(fromFile: fixture.sentryPath, origin: SentryTraceOriginAutoNSData) { _ in + let _ = sut.measureNSData(fromFile: fixture.sentryPath, origin: "custom.origin") { _ in span = self.firstSpan(transaction) expect.fulfill() return nil @@ -341,37 +341,51 @@ class SentryFileIOTrackerTests: XCTestCase { return result?.first } - private func assertDataSpan(_ span: Span?, path: String, operation: String, size: Int, mainThread: Bool = true ) { - XCTAssertNotNil(span) - XCTAssertEqual(span?.operation, operation) - XCTAssertEqual(span?.origin, "auto.file.ns_data") - XCTAssertTrue(span?.isFinished ?? false) - XCTAssertEqual(span?.data["file.size"] as? Int, size) - XCTAssertEqual(span?.data["file.path"] as? String, path) + private func assertDataSpan( + _ span: Span?, + path: String, + operation: String, + size: Int, + origin: String, + mainThread: Bool = true, + file: StaticString = #file, + line: UInt = #line + ) { + XCTAssertNotNil(span, file: file, line: line) + XCTAssertEqual(span?.operation, operation, file: file, line: line) + XCTAssertEqual(span?.origin, origin, file: file, line: line) + XCTAssertTrue(span?.isFinished ?? false, file: file, line: line) + XCTAssertEqual(span?.data["file.size"] as? Int, size, file: file, line: line) + XCTAssertEqual(span?.data["file.path"] as? String, path, file: file, line: line) XCTAssertEqual(span?.data["blocked_main_thread"] as? Bool ?? false, mainThread) if mainThread { guard let frames = (span as? SentrySpan)?.frames else { - XCTFail("File IO Span in the main thread has no frames") + XCTFail("File IO Span in the main thread has no frames", file: file, line: line) return } - XCTAssertEqual(frames.first, TestData.mainFrame) - XCTAssertEqual(frames.last, TestData.testFrame) + XCTAssertEqual(frames.first, TestData.mainFrame, file: file, line: line) + XCTAssertEqual(frames.last, TestData.testFrame, file: file, line: line) } let lastComponent = (path as NSString).lastPathComponent if operation == SentrySpanOperationFileRead { - XCTAssertEqual(span?.spanDescription, lastComponent) + XCTAssertEqual(span?.spanDescription, lastComponent, file: file, line: line) } else { let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) - XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))") + XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))", file: file, line: line) } } - private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval) { + private func assertSpanDuration( + span: Span?, + expectedDuration: TimeInterval, + file: StaticString = #file, + line: UInt = #line + ) { let duration = span?.timestamp?.timeIntervalSince(span!.startTimestamp!) - XCTAssertEqual(duration, expectedDuration) + XCTAssertEqual(duration, expectedDuration, file: file, line: line) } private func advanceTime(bySeconds: TimeInterval) { diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index 0bb133cf95c..8e2885907d5 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -110,11 +110,11 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - func testDataWrapper_Writing_Tracking() throws { + func testDataExtension_Writing_Tracking() throws { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. - // Therefore, the wrapper is only tested with these OS versions. + // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { @@ -122,11 +122,11 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - func testDataWrapper_WritingWithOption_Tracking() throws { + func testDataExtension_WritingWithOption_Tracking() throws { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. - // Therefore, the wrapper is only tested with these OS versions. + // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { @@ -192,11 +192,11 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - func testDataWrapper_ReadingURL_Tracking() throws { + func testDataExtension_ReadingURL_Tracking() throws { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. - // Therefore, the wrapper is only tested with these OS versions. + // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { @@ -205,11 +205,11 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - func testDataWrapper_ReadingURLWithOption_Tracking() throws { + func testDataExtension_ReadingURLWithOption_Tracking() throws { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. - // Therefore, the wrapper is only tested with these OS versions. + // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { @@ -310,7 +310,14 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - private func assertSpans( _ spansCount: Int, _ operation: String, _ description: String = "TestFile", _ block: () -> Void) { + private func assertSpans( + _ spansCount: Int, + _ operation: String, + _ description: String = "TestFile", + _ block: () -> Void, + file: StaticString = #file, + line: UInt = #line + ) { let parentTransaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) block() @@ -318,13 +325,13 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { let childrenSelector = NSSelectorFromString("children") guard let children = parentTransaction.perform(childrenSelector).takeUnretainedValue() as? [Span] else { - XCTFail("Did not found children property from transaction.") + XCTFail("Did not found children property from transaction.", file: file, line: line) return } - XCTAssertEqual(children.count, spansCount) + XCTAssertEqual(children.count, spansCount, file: file, line: line) if let first = children.first { - XCTAssertEqual(first.operation, operation) + XCTAssertEqual(first.operation, operation, file: file, line: line) } } } From 1e4c13ebae90442681af5c6c6e1e24fa380f59a3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 13:11:55 +0100 Subject: [PATCH 17/38] add file io tracker to dependency container --- Sentry.xcodeproj/project.pbxproj | 8 ++++---- Sources/Sentry/SentryDependencyContainer.m | 16 ++++++++++++++++ Sources/Sentry/SentryFileIOTracker.m | 5 +---- .../Sentry/SentryFileIOTrackingIntegration.m | 4 +--- Sources/Sentry/SentryNSDataSwizzling.m | 10 +++++----- Sources/Sentry/SentryNSFileManagerSwizzling.m | 2 +- .../HybridPublic/SentryDependencyContainer.h | 2 ++ Sources/Sentry/include/SentryFileIOTracker.h | 7 +++++++ Sources/Sentry/include/SentryTraceOrigins.h | 1 - ...aceOrigins.swift => SentryTraceOrigin.swift} | 3 ++- .../SentryFileIOTrackingIntegrationTests.swift | 17 +++++++++++++++-- 11 files changed, 54 insertions(+), 21 deletions(-) rename Sources/Swift/Transactions/{SentryTraceOrigins.swift => SentryTraceOrigin.swift} (62%) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index a3fc09cf174..9e5cd28877e 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,7 +789,7 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; - D48724DB2D352597005DE483 /* SentryTraceOrigins.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */; }; + D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; @@ -1879,7 +1879,7 @@ A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; - D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigins.swift; sourceTree = ""; }; + D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; @@ -3665,7 +3665,7 @@ D48724D92D35258A005DE483 /* Transactions */ = { isa = PBXGroup; children = ( - D48724DA2D352591005DE483 /* SentryTraceOrigins.swift */, + D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */, ); path = Transactions; sourceTree = ""; @@ -4910,7 +4910,7 @@ 7BC9A20628F41781001E7C4C /* SentryMeasurementUnit.m in Sources */, 63FE71A020DA4C1100CDBAE8 /* SentryCrashInstallation.m in Sources */, 63FE713520DA4C1100CDBAE8 /* SentryCrashMemory.c in Sources */, - D48724DB2D352597005DE483 /* SentryTraceOrigins.swift in Sources */, + D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */, 63FE714520DA4C1100CDBAE8 /* SentryCrashObjC.c in Sources */, 63FE710520DA4C1000CDBAE8 /* SentryAsyncSafeLog.c in Sources */, 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 1e6c3cb1313..2095575cf1c 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -5,6 +5,7 @@ #import "SentryDispatchQueueWrapper.h" #import "SentryDisplayLinkWrapper.h" #import "SentryExtraContextProvider.h" +#import "SentryFileIOTracker.h" #import "SentryFileManager.h" #import "SentryInternalCDefines.h" #import "SentryLog.h" @@ -186,6 +187,21 @@ - (SentryThreadInspector *)threadInspector SENTRY_DISABLE_THREAD_SANITIZER( return _threadInspector; } +- (SentryFileIOTracker *)fileIOTracker SENTRY_DISABLE_THREAD_SANITIZER( + "double-checked lock produce false alarms") +{ + if (_fileIOTracker == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_fileIOTracker == nil) { + _fileIOTracker = + [[SentryFileIOTracker alloc] initWithThreadInspector:[self threadInspector] + processInfoWrapper:[self processInfoWrapper]]; + } + } + } + return _fileIOTracker; +} + - (SentryDebugImageProvider *)debugImageProvider { @synchronized(sentryDependencyContainerLock) { diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index baaf04ba74a..f5856769e71 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -34,10 +34,7 @@ @implementation SentryFileIOTracker + (instancetype)sharedInstance { - SentryFileIOTracker *tracker = [[SentryFileIOTracker alloc] - initWithThreadInspector:SentryDependencyContainer.sharedInstance.threadInspector - processInfoWrapper:SentryDependencyContainer.sharedInstance.processInfoWrapper]; - return tracker; + return SentryDependencyContainer.sharedInstance.fileIOTracker; } - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index 5c650ca7ce4..d634e416fff 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -19,9 +19,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } - self.tracker = [[SentryFileIOTracker alloc] - initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] - processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; + self.tracker = [[SentryDependencyContainer sharedInstance] fileIOTracker]; [self.tracker enable]; [SentryNSDataSwizzling.shared startWithOptions:options tracker:self.tracker]; diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 567437e3709..85d47e32338 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -46,7 +46,7 @@ + (void)swizzle measureNSData:self writeToFile:path atomically:useAuxiliaryFile - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] method:^BOOL(NSString *_Nonnull filePath, BOOL isAtomically) { return SentrySWCallOriginal(filePath, isAtomically); }]; @@ -62,7 +62,7 @@ + (void)swizzle measureNSData:self writeToFile:path options:writeOptionsMask - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] error:error method:^BOOL( NSString *filePath, NSDataWritingOptions options, NSError **outError) { @@ -80,7 +80,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path options:options - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] error:error method:^NSData *(NSString *filePath, NSDataReadingOptions options, NSError **outError) { @@ -95,7 +95,7 @@ + (void)swizzle SentrySWReturnType(NSData *), SentrySWArguments(NSString * path), SentrySWReplacement({ return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] method:^NSData *( NSString *filePath) { return SentrySWCallOriginal(filePath); }]; }), @@ -110,7 +110,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromURL:url options:options - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] error:error method:^NSData *(NSURL *fileUrl, NSDataReadingOptions options, NSError **outError) { diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index c341e41dceb..32651207a6d 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -60,7 +60,7 @@ + (void)swizzle measureNSFileManagerCreateFileAtPath:path data:data attributes:attributes - origin:SentryTraceOriginAutoNSData + origin:[SentryTraceOrigin autoNSData] method:^BOOL(NSString *path, NSData *data, NSDictionary *attributes) { diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index 433cace15e7..0ee1a14506f 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -22,6 +22,7 @@ @class SentrySystemWrapper; @class SentryThreadWrapper; @class SentryThreadInspector; +@class SentryFileIOTracker; @protocol SentryRandom; @protocol SentryCurrentDateProvider; @protocol SentryRateLimits; @@ -77,6 +78,7 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentrySysctl *sysctlWrapper; @property (nonatomic, strong) SentryThreadInspector *threadInspector; @property (nonatomic, strong) id rateLimits; +@property (nonatomic, strong) SentryFileIOTracker *fileIOTracker; #if SENTRY_UIKIT_AVAILABLE @property (nonatomic, strong) SentryFramesTracker *framesTracker; diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index b1fb0889c18..833c9cbdf22 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -9,6 +9,13 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryFileIOTracker : NSObject SENTRY_NO_INIT +/** + * Convenience accessor to the shared instance of the tracker in the dependency container. + * + * @note Can be used from Swift without import the entire dependency container. + * + * @return The shared instance of the tracker. + */ + (instancetype)sharedInstance; - (instancetype)initWithThreadInspector:(SentryThreadInspector *)threadInspector diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 7922cdb9a7c..29bef94fea5 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -5,7 +5,6 @@ static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker static NSString *const SentryTraceOriginAutoAppStart = @"auto.app.start"; static NSString *const SentryTraceOriginAutoAppStartProfile = @"auto.app.start.profile"; -static NSString *const SentryTraceOriginAutoNSData = @"auto.file.ns_data"; static NSString *const SentryTraceOriginAutoDBCoreData = @"auto.db.core_data"; static NSString *const SentryTraceOriginAutoHttpNSURLSession = @"auto.http.ns_url_session"; static NSString *const SentryTraceOriginAutoUIViewController = @"auto.ui.view_controller"; diff --git a/Sources/Swift/Transactions/SentryTraceOrigins.swift b/Sources/Swift/Transactions/SentryTraceOrigin.swift similarity index 62% rename from Sources/Swift/Transactions/SentryTraceOrigins.swift rename to Sources/Swift/Transactions/SentryTraceOrigin.swift index d04c9d799b8..d43d9d0b104 100644 --- a/Sources/Swift/Transactions/SentryTraceOrigins.swift +++ b/Sources/Swift/Transactions/SentryTraceOrigin.swift @@ -1,6 +1,7 @@ // This file is the Swift addition for `SentryTraceOrigins.h` in the `Sentry` module. @objcMembers -public class SentryTraceOrigin { +public class SentryTraceOrigin: NSObject { + static let autoNSData = "auto.file.ns_data" static let manualData = "manual.file.data" } diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index 8e2885907d5..c67258ab876 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -27,6 +27,14 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { fileURL = fileDirectory.appendingPathComponent("TestFile") filePath = fileURL?.path } + + func assertDataWritten(toUrl url: URL, file: StaticString = #file, line: UInt = #line) { + guard let data = try? Data(contentsOf: url) else { + XCTFail("Could not load written resource file", file: file, line: line) + return + } + XCTAssertEqual(self.data, data, file: file, line: line) + } } private var fixture: Fixture! @@ -111,15 +119,19 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func testDataExtension_Writing_Tracking() throws { + // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) + + // -- Act & Assert -- assertSpans(1, "file.write") { try? fixture.data.writeWithSentryTracing(to: fixture.fileURL) } + fixture.assertDataWritten(toUrl: fixture.fileURL) } func testDataExtension_WritingWithOption_Tracking() throws { @@ -133,6 +145,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { try? fixture.data .writeWithSentryTracing(to: fixture.fileURL, options: .atomic) } + fixture.assertDataWritten(toUrl: fixture.fileURL) } func test_ReadingTrackingDisabled_forIOOption() { @@ -329,9 +342,9 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { return } - XCTAssertEqual(children.count, spansCount, file: file, line: line) + XCTAssertEqual(children.count, spansCount, "Actual span count is not equal to expected count", file: file, line: line) if let first = children.first { - XCTAssertEqual(first.operation, operation, file: file, line: line) + XCTAssertEqual(first.operation, operation, "Operation for span is not equal to expected operation", file: file, line: line) } } } From 335207c0cca5a37743817431c2faf1dcd8200830 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 14:27:40 +0100 Subject: [PATCH 18/38] refactor changes --- CHANGELOG.md | 7 +- Sentry.xcodeproj/project.pbxproj | 20 +- Sources/Sentry/SentryFileIOTracker.m | 4 +- Sources/Sentry/SentryFileOperations.m | 3 - Sources/Sentry/SentryNSDataSwizzling.m | 11 +- Sources/Sentry/SentryNSFileManagerSwizzling.m | 3 +- Sources/Sentry/include/SentrySpanOperations.h | 4 +- Sources/Sentry/include/SentryTraceOrigins.h | 3 + .../Transactions/SentrySpanOperation.swift | 8 + .../Transactions/SentryTraceOrigin.swift | 3 +- .../Helper/SentryDataWrapperTests.swift | 364 ------------------ .../IO/SentryFileIOTrackerTests.swift | 20 +- ...SentryFileIOTrackingIntegrationObjCTests.m | 26 +- .../IO/SentryNSFileManagerSwizzlingTests.m | 10 +- .../SentrySpanOperationTests.swift | 12 + 15 files changed, 85 insertions(+), 413 deletions(-) create mode 100644 Sources/Swift/Transactions/SentrySpanOperation.swift delete mode 100644 Tests/SentryTests/Helper/SentryDataWrapperTests.swift create mode 100644 Tests/SentryTests/Transactions/SentrySpanOperationTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 90523614898..48295d209c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +## Improvements + +- Add file IO tracking wrapper for Swift.Data (#4605) + ## 8.43.1-beta.0 ### Fixes @@ -33,7 +39,6 @@ ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) -- Add file IO tracking wrapper for Swift.Data (#4605) - Mask screenshots for errors (#4623) - Slightly speed up serializing scope (#4661) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 9e5cd28877e..62bf152e3ef 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -788,8 +788,9 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; - D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; + D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; + D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; @@ -1878,8 +1879,9 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; - D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDataWrapperTests.swift; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; + D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; + D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; @@ -2540,6 +2542,7 @@ 63AA75931EB8AEDB00D153DE /* SentryTests */ = { isa = PBXGroup; children = ( + D48724DE2D3549C1005DE483 /* Transactions */, D4F2B5332D0C69CC00649E42 /* Recording */, 62872B602BA1B84400A4FA7D /* Swift */, 7B3878E92490D90400EBDEA2 /* SentryClient+TestInit.h */, @@ -3084,7 +3087,6 @@ isa = PBXGroup; children = ( D8AE48BE2C578D540092A2A6 /* SentryLog.swift */, - D4252AE22D119D3B00184F6F /* SentryDataWrapperTests.swift */, 849AC3FF29E0C1FF00889C16 /* SentryFormatterTests.swift */, 7B88F30324BC8E6500ADF90A /* SentrySerializationTests.swift */, 62F4DDA02C04CB9700588890 /* SentryBaggageSerializationTests.swift */, @@ -3665,11 +3667,20 @@ D48724D92D35258A005DE483 /* Transactions */ = { isa = PBXGroup; children = ( + D48724DC2D354934005DE483 /* SentrySpanOperation.swift */, D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */, ); path = Transactions; sourceTree = ""; }; + D48724DE2D3549C1005DE483 /* Transactions */ = { + isa = PBXGroup; + children = ( + D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */, + ); + path = Transactions; + sourceTree = ""; + }; D4F2B5332D0C69CC00649E42 /* Recording */ = { isa = PBXGroup; children = ( @@ -4624,6 +4635,7 @@ 624688192C048EF10006179C /* SentryBaggageSerialization.swift in Sources */, D81988C92BEC19200020E36C /* SentryRRWebBreadcrumbEvent.swift in Sources */, 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, + D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */, 620203B22C59025E0008317C /* SentryFileContents.swift in Sources */, 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, @@ -4977,7 +4989,6 @@ 7B98D7EC25FB7C4900C5A389 /* SentryAppStateTests.swift in Sources */, 62A3C7BE2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift in Sources */, 8EE017A126704CD500470616 /* SentryUIViewControllerPerformanceTrackerTests.swift in Sources */, - D4252AE32D119D3F00184F6F /* SentryDataWrapperTests.swift in Sources */, 7B5B94352657AD21002E474B /* SentryFramesTrackingIntegrationTests.swift in Sources */, 8431EE5B29ADB8EA00D8DC56 /* SentryTimeTests.m in Sources */, 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */, @@ -5183,6 +5194,7 @@ 7BBD18B32451805500427C76 /* SentryRateLimitsParserTests.swift in Sources */, 7B82722B27A3220A00F4BFF4 /* SentryFileIoTrackingUnitTests.swift in Sources */, 7BF9EF7A2722B58900B5BBEF /* SentrySubClassFinderTests.swift in Sources */, + D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */, 7B59398224AB47650003AAD2 /* SentrySessionTrackerTests.swift in Sources */, 7B05A61824A4D14A00EF211D /* SentrySessionGeneratorTests.swift in Sources */, D8CB742B294B1DD000A5F964 /* SentryUIApplicationTests.swift in Sources */, diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index f5856769e71..ad9ec464186 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -271,7 +271,7 @@ - (void)mainThreadExtraInfo:(id)span { return [self spanForPath:path origin:origin - operation:SentrySpanOperationFileWrite + operation:SentrySpanOperation.fileWrite size:data.length]; } @@ -287,7 +287,7 @@ - (void)mainThreadExtraInfo:(id)span if (count) return nil; - return [self spanForPath:path origin:origin operation:SentrySpanOperationFileRead size:0]; + return [self spanForPath:path origin:origin operation:SentrySpanOperation.fileRead size:0]; } - (void)endTrackingFile diff --git a/Sources/Sentry/SentryFileOperations.m b/Sources/Sentry/SentryFileOperations.m index 0abf9b082b0..b28db44f3b0 100644 --- a/Sources/Sentry/SentryFileOperations.m +++ b/Sources/Sentry/SentryFileOperations.m @@ -1,8 +1,5 @@ #import "SentrySpanOperations.h" -NSString *const SentrySpanOperationFileWrite = @"file.write"; -NSString *const SentrySpanOperationFileRead = @"file.read"; - NSString *const SentrySpanOperationUILoad = @"ui.load"; NSString *const SentrySpanOperationUILoadInitialDisplay = @"ui.load.initial_display"; NSString *const SentrySpanOperationUILoadFullDisplay = @"ui.load.full_display"; diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 85d47e32338..540021b97b2 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -1,7 +1,6 @@ #import "SentryNSDataSwizzling.h" #import "SentryLog.h" #import "SentrySwizzle.h" -#import "SentryTraceOrigins.h" #import @interface SentryNSDataSwizzling () @@ -46,7 +45,7 @@ + (void)swizzle measureNSData:self writeToFile:path atomically:useAuxiliaryFile - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData method:^BOOL(NSString *_Nonnull filePath, BOOL isAtomically) { return SentrySWCallOriginal(filePath, isAtomically); }]; @@ -62,7 +61,7 @@ + (void)swizzle measureNSData:self writeToFile:path options:writeOptionsMask - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData error:error method:^BOOL( NSString *filePath, NSDataWritingOptions options, NSError **outError) { @@ -80,7 +79,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path options:options - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData error:error method:^NSData *(NSString *filePath, NSDataReadingOptions options, NSError **outError) { @@ -95,7 +94,7 @@ + (void)swizzle SentrySWReturnType(NSData *), SentrySWArguments(NSString * path), SentrySWReplacement({ return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData method:^NSData *( NSString *filePath) { return SentrySWCallOriginal(filePath); }]; }), @@ -110,7 +109,7 @@ + (void)swizzle return [SentryNSDataSwizzling.shared.tracker measureNSDataFromURL:url options:options - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData error:error method:^NSData *(NSURL *fileUrl, NSDataReadingOptions options, NSError **outError) { diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 32651207a6d..d4064bd6ee0 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -1,7 +1,6 @@ #import "SentryNSFileManagerSwizzling.h" #import "SentryLog.h" #import "SentrySwizzle.h" -#import "SentryTraceOrigins.h" #import @interface SentryNSFileManagerSwizzling () @@ -60,7 +59,7 @@ + (void)swizzle measureNSFileManagerCreateFileAtPath:path data:data attributes:attributes - origin:[SentryTraceOrigin autoNSData] + origin:SentryTraceOrigin.autoNSData method:^BOOL(NSString *path, NSData *data, NSDictionary *attributes) { diff --git a/Sources/Sentry/include/SentrySpanOperations.h b/Sources/Sentry/include/SentrySpanOperations.h index 4e5543ab9af..99f527a7b23 100644 --- a/Sources/Sentry/include/SentrySpanOperations.h +++ b/Sources/Sentry/include/SentrySpanOperations.h @@ -1,8 +1,8 @@ #import "SentryDefines.h" #import -SENTRY_EXTERN NSString *const SentrySpanOperationFileWrite; -SENTRY_EXTERN NSString *const SentrySpanOperationFileRead; +// Note: Consider adding new operations to the `SentrySpanOperation` enum in +// `SentrySpanOperations.swift` instead of adding them here. SENTRY_EXTERN NSString *const SentrySpanOperationUILoad; SENTRY_EXTERN NSString *const SentrySpanOperationUILoadInitialDisplay; diff --git a/Sources/Sentry/include/SentryTraceOrigins.h b/Sources/Sentry/include/SentryTraceOrigins.h index 29bef94fea5..c810b9f4e59 100644 --- a/Sources/Sentry/include/SentryTraceOrigins.h +++ b/Sources/Sentry/include/SentryTraceOrigins.h @@ -1,5 +1,8 @@ #import +// Note: Consider adding new operations to the `SentrySpanOperation` enum in +// `SentrySpanOperations.swift` instead of adding them here. + static NSString *const SentryTraceOriginManual = @"manual"; static NSString *const SentryTraceOriginUIEventTracker = @"auto.ui.event_tracker"; diff --git a/Sources/Swift/Transactions/SentrySpanOperation.swift b/Sources/Swift/Transactions/SentrySpanOperation.swift new file mode 100644 index 00000000000..99bb612f88e --- /dev/null +++ b/Sources/Swift/Transactions/SentrySpanOperation.swift @@ -0,0 +1,8 @@ +// This file is the Swift addition for `SentrySpanOperations.h` in the `Sentry` module. + +/// - Note: Consider adding new operations here, as they are available in Swift without importing the `Sentry` module. +@objcMembers +class SentrySpanOperation: NSObject { + static let fileRead = "file.read" + static let fileWrite = "file.write" +} diff --git a/Sources/Swift/Transactions/SentryTraceOrigin.swift b/Sources/Swift/Transactions/SentryTraceOrigin.swift index d43d9d0b104..d3d63074620 100644 --- a/Sources/Swift/Transactions/SentryTraceOrigin.swift +++ b/Sources/Swift/Transactions/SentryTraceOrigin.swift @@ -1,7 +1,8 @@ // This file is the Swift addition for `SentryTraceOrigins.h` in the `Sentry` module. +/// - Note: Consider adding new origins here, as they are available in Swift without importing the `Sentry` module. @objcMembers -public class SentryTraceOrigin: NSObject { +class SentryTraceOrigin: NSObject { static let autoNSData = "auto.file.ns_data" static let manualData = "manual.file.data" } diff --git a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift b/Tests/SentryTests/Helper/SentryDataWrapperTests.swift deleted file mode 100644 index 4b8f245d0c2..00000000000 --- a/Tests/SentryTests/Helper/SentryDataWrapperTests.swift +++ /dev/null @@ -1,364 +0,0 @@ -@testable import Sentry -import SentryTestUtils -import XCTest - -class SentryDataWrapperTests: XCTestCase { - private class Fixture { - - let filePath = "Some Path" - let sentryPath = try! TestFileManager(options: Options()).sentryPath - let dateProvider = TestCurrentDateProvider() - let data = "SOME DATA".data(using: .utf8)! - let threadInspector = TestThreadInspector.instance - let imageProvider = TestDebugImageProvider() - - func getSut() -> SentryFileIOTracker { - imageProvider.debugImages = [TestData.debugImage] - SentryDependencyContainer.sharedInstance().debugImageProvider = imageProvider - - threadInspector.allThreads = [TestData.thread2] - - let processInfoWrapper = TestSentryNSProcessInfoWrapper() - processInfoWrapper.overrides.processDirectoryPath = "sentrytest" - - let result = SentryFileIOTracker(threadInspector: threadInspector, processInfoWrapper: processInfoWrapper) - SentryDependencyContainer.sharedInstance().dateProvider = dateProvider - result.enable() - return result - } - } - - private var fixture: Fixture! - - override func setUp() { - super.setUp() - fixture = Fixture() - fixture.getSut().enable() - SentrySDK.start { - $0.removeAllIntegrations() - } - } - - override func tearDown() { - super.tearDown() - clearTestState() - } - - func testWritePathAtomically() { - let sut = fixture.getSut() - var methodPath: String? - var methodAuxiliareFile: Bool? - - var result = sut.measure( - fixture.data, - writeToFile: fixture.filePath, - atomically: false, - origin: "custom.origin" - ) { path, useAuxiliareFile -> Bool in - methodPath = path - methodAuxiliareFile = useAuxiliareFile - return false - } - - XCTAssertEqual(fixture.filePath, methodPath) - XCTAssertFalse(methodAuxiliareFile!) - XCTAssertFalse(result) - - result = sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: true, - origin: "custom.origin") { _, useAuxiliareFile -> Bool in - methodAuxiliareFile = useAuxiliareFile - return true - } - - XCTAssertTrue(methodAuxiliareFile!) - XCTAssertTrue(result) - } - - func testWritePathOptionsError() { - let sut = fixture.getSut() - var methodPath: String? - var methodOptions: NSData.WritingOptions? - var methodError: NSError? - - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, - origin: "custom.origin") { path, writingOption, _ -> Bool in - methodPath = path - methodOptions = writingOption - return true - } - - XCTAssertEqual(fixture.filePath, methodPath) - XCTAssertEqual(methodOptions, .atomic) - - do { - try sut.measure(fixture.data, writeToFile: fixture.filePath, options: .withoutOverwriting, - origin: "custom.origin") { _, writingOption, errorPointer -> Bool in - methodOptions = writingOption - errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) - return false - } - } catch { - methodError = error as NSError? - } - - XCTAssertEqual(methodOptions, .withoutOverwriting) - XCTAssertEqual(methodError?.domain, "Test Error") - } - - func testWriteAtomically_CheckTrace() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: "custom.origin") { _, _ -> Bool in - span = self.firstSpan(transaction) - XCTAssertFalse(span?.isFinished ?? true) - self.advanceTime(bySeconds: 4) - return true - } - - assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan( - span, - path: fixture.filePath, - operation: SentrySpanOperationFileWrite, - size: fixture.data.count, origin: "custom.origin" - ) - } - - func testWriteAtomically_CheckTransaction_DebugImages() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: "custom.origin") { _, _ -> Bool in - span = self.firstSpan(transaction) - XCTAssertFalse(span?.isFinished ?? true) - self.advanceTime(bySeconds: 4) - return true - } - - let transactionEvent = Dynamic(transaction).toTransaction().asObject as? Transaction - - XCTAssertNotNil(transactionEvent?.debugMeta) - XCTAssertTrue(transactionEvent?.debugMeta?.count ?? 0 > 0) - XCTAssertEqual(transactionEvent?.debugMeta?.first, TestData.debugImage) - } - - func testWriteAtomically_CheckTransaction_FilterOut_nonProcessFrames() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - - let stackTrace = SentryStacktrace(frames: [TestData.mainFrame, TestData.testFrame, TestData.outsideFrame], registers: ["register": "one"]) - let thread = SentryThread(threadId: 0) - thread.stacktrace = stackTrace - fixture.threadInspector.allThreads = [thread] - - var span: SentrySpan? - - sut.measure(fixture.data, writeToFile: fixture.filePath, atomically: false, - origin: "custom.origin") { _, _ -> Bool in - span = self.firstSpan(transaction) as? SentrySpan - XCTAssertFalse(span?.isFinished ?? true) - return true - } - - XCTAssertEqual(span?.frames?.count ?? 0, 2) - XCTAssertEqual(span?.frames?.first, TestData.mainFrame) - XCTAssertEqual(span?.frames?.last, TestData.testFrame) - } - - func testWriteAtomically_Background() { - let sut = self.fixture.getSut() - let expect = expectation(description: "Operation in background thread") - DispatchQueue.global(qos: .default).async { - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - sut.measure(self.fixture.data, writeToFile: self.fixture.filePath, atomically: false, - origin: "custom.origin") { _, _ -> Bool in - span = self.firstSpan(transaction) - XCTAssertFalse(span?.isFinished ?? true) - self.advanceTime(bySeconds: 4) - return true - } - - self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, origin: "custom.origin", mainThread: false) - expect.fulfill() - } - - wait(for: [expect], timeout: 0.1) - } - - func testWriteWithOptionsAndError_CheckTrace() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - try! sut.measure(fixture.data, writeToFile: fixture.filePath, options: .atomic, - origin: "custom.origin") { _, _, _ -> Bool in - span = self.firstSpan(transaction) - XCTAssertFalse(span?.isFinished ?? true) - self.advanceTime(bySeconds: 3) - return true - } - - assertSpanDuration(span: span, expectedDuration: 3) - assertDataSpan( - span, - path: fixture.filePath, - operation: SentrySpanOperationFileWrite, - size: fixture.data.count, origin: "custom.origin" - ) - } - - func testDontTrackSentryFilesWrites() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - let expect = expectation(description: "") - try! sut.measure(fixture.data, writeToFile: fixture.sentryPath, options: .atomic, - origin: "custom.origin") { _, _, _ -> Bool in - span = self.firstSpan(transaction) - expect.fulfill() - return true - } - - XCTAssertNil(span) - wait(for: [expect], timeout: 0.1) - } - - func testReadFromString() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - var usedPath: String? - - let data = sut.measureNSData(fromFile: fixture.filePath, - origin: "custom.origin") { path in - span = self.firstSpan(transaction) - usedPath = path - return self.fixture.data - } - - XCTAssertEqual(usedPath, fixture.filePath) - XCTAssertEqual(data?.count, fixture.data.count) - - assertDataSpan( - span, - path: fixture.filePath, - operation: SentrySpanOperationFileRead, - size: fixture.data.count, origin: "custom.origin" - ) - } - - func testReadFromStringOptionsError() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - var usedPath: String? - var usedOptions: NSData.ReadingOptions? - - let data = try? sut.measureNSData(fromFile: self.fixture.filePath, options: .uncached, - origin: "custom.origin") { path, options, _ -> Data in - span = self.firstSpan(transaction) - usedOptions = options - usedPath = path - return self.fixture.data - } - - XCTAssertEqual(usedPath, fixture.filePath) - XCTAssertEqual(data?.count, fixture.data.count) - XCTAssertEqual(usedOptions, .uncached) - - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") - } - - func testReadFromURLOptionsError() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - var usedUrl: URL? - let url = URL(fileURLWithPath: fixture.filePath) - var usedOptions: NSData.ReadingOptions? - - let data = try? sut.measureNSData(from: url, options: .uncached, - origin: "custom.origin") { url, options, _ in - span = self.firstSpan(transaction) - usedOptions = options - usedUrl = url - return self.fixture.data - } - - XCTAssertEqual(usedUrl, url) - XCTAssertEqual(data?.count, fixture.data.count) - XCTAssertEqual(usedOptions, .uncached) - - assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") - } - - func testDontTrackSentryFilesRead() { - let sut = fixture.getSut() - let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - var span: Span? - - let expect = expectation(description: "") - let _ = sut.measureNSData(fromFile: fixture.sentryPath, - origin: "custom.origin") { _ in - span = self.firstSpan(transaction) - expect.fulfill() - return nil - } - - XCTAssertNil(span) - wait(for: [expect], timeout: 0.1) - } - - // MARK: - Helpers - - private func firstSpan(_ transaction: Span) -> Span? { - let result = Dynamic(transaction).children as [Span]? - return result?.first - } - - private func assertDataSpan(_ span: Span?, path: String, operation: String, size: Int, origin: String, mainThread: Bool = true, file: StaticString = #file, line: UInt = #line) { - XCTAssertNotNil(span, file: file, line: line) - XCTAssertEqual(span?.operation, operation, file: file, line: line) - XCTAssertEqual(span?.origin, origin, file: file, line: line) - XCTAssertTrue(span?.isFinished ?? false, file: file, line: line) - XCTAssertEqual(span?.data["file.size"] as? Int, size, file: file, line: line) - XCTAssertEqual(span?.data["file.path"] as? String, path, file: file, line: line) - XCTAssertEqual(span?.data["blocked_main_thread"] as? Bool ?? false, mainThread, file: file, line: line) - - if mainThread { - guard let frames = (span as? SentrySpan)?.frames else { - XCTFail("File IO Span in the main thread has no frames", file: file, line: line) - return - } - XCTAssertEqual(frames.first, TestData.mainFrame, file: file, line: line) - XCTAssertEqual(frames.last, TestData.testFrame, file: file, line: line) - } - - let lastComponent = (path as NSString).lastPathComponent - - if operation == SentrySpanOperationFileRead { - XCTAssertEqual(span?.spanDescription, lastComponent, file: file, line: line) - } else { - let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) - XCTAssertEqual(span?.spanDescription ?? "", "\(lastComponent) (\(bytesDescription))", file: file, line: line) - } - } - - private func assertSpanDuration(span: Span?, expectedDuration: TimeInterval, file: StaticString = #file, line: UInt = #line) { - let duration = span?.timestamp?.timeIntervalSince(span!.startTimestamp!) - XCTAssertEqual(duration, expectedDuration, file: file, line: line) - } - - private func advanceTime(bySeconds: TimeInterval) { - fixture.dateProvider.setDate(date: fixture.dateProvider.date().addingTimeInterval(bySeconds)) - } -} diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index 1890995a02e..2e8a452b247 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -47,10 +47,10 @@ class SentryFileIOTrackerTests: XCTestCase { func testConstants() { //A test to ensure this constants don't accidentally change - XCTAssertEqual("file.read", SentrySpanOperationFileRead) + XCTAssertEqual("file.read", SentrySpanOperation.fileRead) XCTAssertEqual( "file.write", - SentrySpanOperationFileWrite + SentrySpanOperation.fileWrite ) } @@ -125,7 +125,7 @@ class SentryFileIOTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 4) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count, origin: "custom.origin") + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperation.fileWrite, size: fixture.data.count, origin: "custom.origin") } func testWriteAtomically_CheckTransaction_DebugImages() { @@ -184,7 +184,7 @@ class SentryFileIOTrackerTests: XCTestCase { } self.assertSpanDuration(span: span, expectedDuration: 4) - self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperationFileWrite, size: self.fixture.data.count, origin: "custom.origin", mainThread: false) + self.assertDataSpan(span, path: self.fixture.filePath, operation: SentrySpanOperation.fileWrite, size: self.fixture.data.count, origin: "custom.origin", mainThread: false) expect.fulfill() } @@ -204,7 +204,7 @@ class SentryFileIOTrackerTests: XCTestCase { } assertSpanDuration(span: span, expectedDuration: 3) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileWrite, size: fixture.data.count, origin: "custom.origin") + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperation.fileWrite, size: fixture.data.count, origin: "custom.origin") } func testDontTrackSentryFilesWrites() { @@ -238,7 +238,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(usedPath, fixture.filePath) XCTAssertEqual(data?.count, fixture.data.count) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperation.fileRead, size: fixture.data.count, origin: "custom.origin") } func testReadFromStringOptionsError() { @@ -259,7 +259,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") + assertDataSpan(span, path: fixture.filePath, operation: SentrySpanOperation.fileRead, size: fixture.data.count, origin: "custom.origin") } func testReadFromURLOptionsError() { @@ -281,7 +281,7 @@ class SentryFileIOTrackerTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) XCTAssertEqual(usedOptions, .uncached) - assertDataSpan(span, path: url.path, operation: SentrySpanOperationFileRead, size: fixture.data.count, origin: "custom.origin") + assertDataSpan(span, path: url.path, operation: SentrySpanOperation.fileRead, size: fixture.data.count, origin: "custom.origin") } func testCreateFile() { @@ -315,7 +315,7 @@ class SentryFileIOTrackerTests: XCTestCase { assertDataSpan( span, path: fixture.filePath, - operation: SentrySpanOperationFileWrite, + operation: SentrySpanOperation.fileWrite, size: fixture.data.count, origin: "custom.origin" ) } @@ -370,7 +370,7 @@ class SentryFileIOTrackerTests: XCTestCase { let lastComponent = (path as NSString).lastPathComponent - if operation == SentrySpanOperationFileRead { + if operation == SentrySpanOperation.fileRead { XCTAssertEqual(span?.spanDescription, lastComponent, file: file, line: line) } else { let bytesDescription = SentryByteCountFormatter.bytesCountDescription( UInt(size)) diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index 2761c972fca..cd7839294b0 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -71,7 +71,7 @@ - (void)tearDown - (void)test_dataWithContentsOfFile { - [self assertTransactionForOperation:SentrySpanOperationFileRead + [self assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[NSData dataWithContentsOfFile:self->filePath]]; @@ -81,7 +81,7 @@ - (void)test_dataWithContentsOfFile - (void)test_dataWithContentsOfFileOptionsError { [self - assertTransactionForOperation:SentrySpanOperationFileRead + assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[NSData @@ -94,7 +94,7 @@ - (void)test_dataWithContentsOfFileOptionsError - (void)test_dataWithContentsOfURL { [self - assertTransactionForOperation:SentrySpanOperationFileRead + assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[NSData dataWithContentsOfURL:self->fileUrl]]; }]; @@ -103,7 +103,7 @@ - (void)test_dataWithContentsOfURL - (void)test_dataWithContentsOfURLOptionsError { [self - assertTransactionForOperation:SentrySpanOperationFileRead + assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[NSData dataWithContentsOfURL:self->fileUrl @@ -114,7 +114,7 @@ - (void)test_dataWithContentsOfURLOptionsError - (void)test_initWithContentsOfURL { - [self assertTransactionForOperation:SentrySpanOperationFileRead + [self assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[[NSData alloc] initWithContentsOfURL:self->fileUrl]]; @@ -123,7 +123,7 @@ - (void)test_initWithContentsOfURL - (void)test_initWithContentsOfFile { - [self assertTransactionForOperation:SentrySpanOperationFileRead + [self assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[[NSData alloc] initWithContentsOfFile:self->filePath]]; @@ -132,7 +132,7 @@ - (void)test_initWithContentsOfFile - (void)test_writeToFileAtomically { - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [self->someData writeToFile:self->filePath atomically:true]; }]; @@ -141,7 +141,7 @@ - (void)test_writeToFileAtomically - (void)test_writeToUrlAtomically { - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [self->someData writeToURL:self->fileUrl atomically:true]; }]; @@ -150,7 +150,7 @@ - (void)test_writeToUrlAtomically - (void)test_writeToFileOptionsError { - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [self->someData writeToFile:self->filePath options:NSDataWritingAtomic @@ -161,7 +161,7 @@ - (void)test_writeToFileOptionsError - (void)test_writeToUrlOptionsError { - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [self->someData writeToURL:self->fileUrl options:NSDataWritingAtomic @@ -172,7 +172,7 @@ - (void)test_writeToUrlOptionsError - (void)test_NSFileManagerContentAtPath { - [self assertTransactionForOperation:SentrySpanOperationFileRead + [self assertTransactionForOperation:SentrySpanOperation.fileRead block:^{ [self assertData:[NSFileManager.defaultManager contentsAtPath:self->filePath]]; @@ -186,7 +186,7 @@ - (void)test_NSFileManagerCreateFile "disable this test until we fix file IO tracking: " "https://github.com/getsentry/sentry-cocoa/issues/4546"); } - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath contents:self->someData @@ -225,7 +225,7 @@ - (void)assertTransactionForOperation:(NSString *)operation block:(void (^)(void NSString *filename = filePath.lastPathComponent; - if ([operation isEqualToString:SentrySpanOperationFileRead]) { + if ([operation isEqualToString:SentrySpanOperation.fileRead]) { XCTAssertEqualObjects(ioSpan.spanDescription, filename); } else { NSString *expectedString = [NSString stringWithFormat:@"%@ (%@)", filename, diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index f8084a1085a..9067236d15e 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -83,7 +83,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabl XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -99,7 +99,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -118,7 +118,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -136,7 +136,7 @@ - (void)testNSFileManagerCreateFile_iOS18macOS15tvOS18OrLater_experimentalFlagEn XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; - [self assertTransactionForOperation:SentrySpanOperationFileWrite + [self assertTransactionForOperation:SentrySpanOperation.fileWrite spanCount:1 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath @@ -179,7 +179,7 @@ - (void)assertTransactionForOperation:(NSString *)operation NSString *filename = filePath.lastPathComponent; - if ([operation isEqualToString:SentrySpanOperationFileRead]) { + if ([operation isEqualToString:SentrySpanOperation.fileRead]) { XCTAssertEqualObjects(ioSpan.spanDescription, filename); } else { NSString *expectedString = [NSString stringWithFormat:@"%@ (%@)", filename, diff --git a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift new file mode 100644 index 00000000000..ffb20e057f4 --- /dev/null +++ b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift @@ -0,0 +1,12 @@ +@testable import Sentry +import XCTest + +class SentrySpanOperationTests: XCTestCase { + + func testFileRead_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanOperation.fileRead, "file.read") + } + func testFileWrite_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanOperation.fileWrite, "file.write") + } +} From b79045867984e7b5c8af60eef6520ba12c153ef0 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 14:33:37 +0100 Subject: [PATCH 19/38] wip --- Sentry.xcodeproj/project.pbxproj | 4 ++++ .../IO/SentryFileIOTrackingIntegrationObjCTests.m | 6 ------ .../Transactions/SentrySpanOperationTests.swift | 3 +++ .../Transactions/SentryTraceOriginTests.swift | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 Tests/SentryTests/Transactions/SentryTraceOriginTests.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 62bf152e3ef..e70fbf2286a 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -791,6 +791,7 @@ D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; + D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; @@ -1882,6 +1883,7 @@ D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; + D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; @@ -3677,6 +3679,7 @@ isa = PBXGroup; children = ( D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */, + D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */, ); path = Transactions; sourceTree = ""; @@ -5186,6 +5189,7 @@ 0A1B497328E597DD00D7BFA3 /* TestLogOutput.swift in Sources */, D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */, 7BA61CBD247BC6B900C130A8 /* TestSentryCrashBinaryImageProvider.swift in Sources */, + D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */, 7BFA69F627E0840400233199 /* SentryANRTrackingIntegrationTests.swift in Sources */, 7BBD18B62451807600427C76 /* SentryDefaultRateLimitsTests.swift in Sources */, 63FE720620DA66EC00CDBAE8 /* SentryCrashMonitor_AppState_Tests.m in Sources */, diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index cd7839294b0..fd435815cd0 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -3,7 +3,6 @@ #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" -#import "SentrySpanOperations.h" #import "SentrySwizzle.h" #import "SentryTracer.h" #import @@ -181,11 +180,6 @@ - (void)test_NSFileManagerContentAtPath - (void)test_NSFileManagerCreateFile { - if (@available(iOS 18, macOS 15, tvOS 15, *)) { - XCTSkip("File IO tracking for Swift.Data is not working for this OS version. Therefore, we " - "disable this test until we fix file IO tracking: " - "https://github.com/getsentry/sentry-cocoa/issues/4546"); - } [self assertTransactionForOperation:SentrySpanOperation.fileWrite block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath diff --git a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift index ffb20e057f4..f5050fc54bf 100644 --- a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift +++ b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift @@ -3,9 +3,12 @@ import XCTest class SentrySpanOperationTests: XCTestCase { + /// This test asserts that the constant matches the SDK specification. func testFileRead_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanOperation.fileRead, "file.read") } + + /// This test asserts that the constant matches the SDK specification. func testFileWrite_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanOperation.fileWrite, "file.write") } diff --git a/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift b/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift new file mode 100644 index 00000000000..1744a5fe5bb --- /dev/null +++ b/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift @@ -0,0 +1,15 @@ +@testable import Sentry +import XCTest + +class SentryTraceOriginTestsTests: XCTestCase { + + /// This test asserts that the constant matches the SDK specification. + func testAutoNSData_shouldBeExpectedValue() { + XCTAssertEqual(SentryTraceOrigin.autoNSData, "auto.file.ns_data") + } + + /// This test asserts that the constant matches the SDK specification. + func testManualData_shouldBeExpectedValue() { + XCTAssertEqual(SentryTraceOrigin.manualData, "manual.file.data") + } +} From 4ff6f4e78135791f01879d7222352fd17e582f6a Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 14:36:04 +0100 Subject: [PATCH 20/38] revert changes in SentrySpanOperations.h --- Sentry.xcodeproj/project.pbxproj | 4 ---- Sources/Sentry/SentryFileOperations.m | 7 ------- Sources/Sentry/include/SentrySpanOperations.h | 11 +++++------ 3 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 Sources/Sentry/SentryFileOperations.m diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index e70fbf2286a..d79cca6e251 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -795,7 +795,6 @@ D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; - D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */; }; D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; @@ -1887,7 +1886,6 @@ D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; - D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryFileOperations.m; sourceTree = ""; }; D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; @@ -3622,7 +3620,6 @@ 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */, 8E4E7C7325DAAB49006AB9E2 /* SentrySpanProtocol.h */, 7B3B83712833832B0001FDEB /* SentrySpanOperations.h */, - D4B3C2D62D158590003E3AB3 /* SentryFileOperations.m */, 622C08D729E546F4002571D4 /* SentryTraceOrigins.h */, 8E4E7C6C25DAAAFE006AB9E2 /* SentrySpan.h */, 84A789092C0E9F5800FF0803 /* SentrySpan+Private.h */, @@ -4881,7 +4878,6 @@ 62B558B02C6B9C3C00C34FEC /* SentryFramesDelayResult.swift in Sources */, 7DAC589123D8B2E0001CF26B /* SentryGlobalEventProcessor.m in Sources */, 7BBD189E244EC8D200427C76 /* SentryRetryAfterHeaderParser.m in Sources */, - D4B3C2D72D158596003E3AB3 /* SentryFileOperations.m in Sources */, 63FE711920DA4C1000CDBAE8 /* SentryCrashMachineContext.c in Sources */, 63FE711B20DA4C1000CDBAE8 /* SentryCrashString.c in Sources */, 7B14089824878F950035403D /* SentryCrashStackEntryMapper.m in Sources */, diff --git a/Sources/Sentry/SentryFileOperations.m b/Sources/Sentry/SentryFileOperations.m deleted file mode 100644 index b28db44f3b0..00000000000 --- a/Sources/Sentry/SentryFileOperations.m +++ /dev/null @@ -1,7 +0,0 @@ -#import "SentrySpanOperations.h" - -NSString *const SentrySpanOperationUILoad = @"ui.load"; -NSString *const SentrySpanOperationUILoadInitialDisplay = @"ui.load.initial_display"; -NSString *const SentrySpanOperationUILoadFullDisplay = @"ui.load.full_display"; -NSString *const SentrySpanOperationUIAction = @"ui.action"; -NSString *const SentrySpanOperationUIActionClick = @"ui.action.click"; diff --git a/Sources/Sentry/include/SentrySpanOperations.h b/Sources/Sentry/include/SentrySpanOperations.h index 99f527a7b23..84bce543947 100644 --- a/Sources/Sentry/include/SentrySpanOperations.h +++ b/Sources/Sentry/include/SentrySpanOperations.h @@ -1,11 +1,10 @@ -#import "SentryDefines.h" #import // Note: Consider adding new operations to the `SentrySpanOperation` enum in // `SentrySpanOperations.swift` instead of adding them here. -SENTRY_EXTERN NSString *const SentrySpanOperationUILoad; -SENTRY_EXTERN NSString *const SentrySpanOperationUILoadInitialDisplay; -SENTRY_EXTERN NSString *const SentrySpanOperationUILoadFullDisplay; -SENTRY_EXTERN NSString *const SentrySpanOperationUIAction; -SENTRY_EXTERN NSString *const SentrySpanOperationUIActionClick; +static NSString *const SentrySpanOperationUILoad = @"ui.load"; +static NSString *const SentrySpanOperationUILoadInitialDisplay = @"ui.load.initial_display"; +static NSString *const SentrySpanOperationUILoadFullDisplay = @"ui.load.full_display"; +static NSString *const SentrySpanOperationUIAction = @"ui.action"; +static NSString *const SentrySpanOperationUIActionClick = @"ui.action.click"; From 4ef283ea0c34f7808e54ca3cc053ad202c35c9a7 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 15:01:30 +0100 Subject: [PATCH 21/38] add more tests, refactored tests --- Sources/Sentry/SentryFileIOTracker.m | 1 - .../IO/SentryFileIOTrackerTests.swift | 89 ++++++++++-- ...SentryFileIOTrackingIntegrationTests.swift | 134 ++++++++++++------ .../IO/SentryNSFileManagerSwizzlingTests.m | 1 - 4 files changed, 169 insertions(+), 56 deletions(-) diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index ad9ec464186..188d05f0570 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -12,7 +12,6 @@ #import "SentrySDK+Private.h" #import "SentryScope+Private.h" #import "SentrySpan.h" -#import "SentrySpanOperations.h" #import "SentrySpanProtocol.h" #import "SentryStacktrace.h" #import "SentryThread.h" diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index 2e8a452b247..c7c3cd63710 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -7,7 +7,9 @@ class SentryFileIOTrackerTests: XCTestCase { private class Fixture { let filePath = "Some Path" - let sentryPath = try! TestFileManager(options: Options()).sentryPath + let fileURL = URL(fileURLWithPath: "Some Path") + let sentryPath = try! TestFileManager(options: Options()).sentryPath + let sentryUrl = URL(fileURLWithPath: try! TestFileManager(options: Options()).sentryPath) let dateProvider = TestCurrentDateProvider() let data = "SOME DATA".data(using: .utf8)! let threadInspector = TestThreadInspector.instance @@ -45,15 +47,6 @@ class SentryFileIOTrackerTests: XCTestCase { clearTestState() } - func testConstants() { - //A test to ensure this constants don't accidentally change - XCTAssertEqual("file.read", SentrySpanOperation.fileRead) - XCTAssertEqual( - "file.write", - SentrySpanOperation.fileWrite - ) - } - func testWritePathAtomically() { let sut = fixture.getSut() var methodPath: String? @@ -223,6 +216,67 @@ class SentryFileIOTrackerTests: XCTestCase { wait(for: [expect], timeout: 0.1) } + func testWriteUrlOptionsError() { + let sut = fixture.getSut() + var methodUrl: URL? + var methodOptions: NSData.WritingOptions? + var methodError: NSError? + + try! sut.measure(fixture.data, writeTo: fixture.fileURL, options: .atomic, origin: "custom.origin") { url, writingOption, _ -> Bool in + methodUrl = url + methodOptions = writingOption + return true + } + + XCTAssertEqual(fixture.fileURL, methodUrl) + XCTAssertEqual(methodOptions, .atomic) + + do { + try sut.measure(fixture.data, writeTo: fixture.fileURL, options: .withoutOverwriting, origin: "custom.origin") { _, writingOption, errorPointer -> Bool in + methodOptions = writingOption + errorPointer?.pointee = NSError(domain: "Test Error", code: -2, userInfo: nil) + return false + } + } catch { + methodError = error as NSError? + } + + XCTAssertEqual(methodOptions, .withoutOverwriting) + XCTAssertEqual(methodError?.domain, "Test Error") + } + + func testWriteUrlWithOptionsAndError_CheckTrace() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + try! sut.measure(fixture.data, writeTo: fixture.fileURL, options: .atomic, origin: "custom.origin") { _, _, _ -> Bool in + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 3) + return true + } + + assertSpanDuration(span: span, expectedDuration: 3) + assertDataSpan(span, url: fixture.fileURL, operation: SentrySpanOperation.fileWrite, size: fixture.data.count, origin: "custom.origin") + } + + func testDontTrackSentryUrlWrites() { + let sut = fixture.getSut() + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + + let expect = expectation(description: "") + try! sut.measure(fixture.data, writeTo: fixture.sentryUrl, options: .atomic, origin: "custom.origin") { _, _, _ -> Bool in + span = self.firstSpan(transaction) + expect.fulfill() + return true + } + + XCTAssertNil(span) + wait(for: [expect], timeout: 0.1) + } + func testReadFromString() { let sut = fixture.getSut() let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) @@ -340,7 +394,20 @@ class SentryFileIOTrackerTests: XCTestCase { let result = Dynamic(transaction).children as [Span]? return result?.first } - + + private func assertDataSpan( + _ span: Span?, + url: URL, + operation: String, + size: Int, + origin: String, + mainThread: Bool = true, + file: StaticString = #file, + line: UInt = #line + ) { + assertDataSpan(span, path: url.path, operation: operation, size: size, origin: origin, file: file, line: line) + } + private func assertDataSpan( _ span: Span?, path: String, diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index c67258ab876..7c020e80381 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -63,30 +63,39 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func test_WritingTrackingDisabled_forIOOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_forSwizzlingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableSwizzling: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_forAutoPerformanceTrackingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableAutoPerformanceTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_WritingTrackingDisabled_TracingDisabled() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(tracesSampleRate: 0)) - + + // -- Assert -- assertWriteWithNoSpans() } func testData_Writing_Tracking() { + // -- Arrange -- let expectedSpanCount: Int if #available(iOS 18, macOS 15, tvOS 18, *) { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 @@ -96,13 +105,18 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } else { expectedSpanCount = 1 } + + // -- Act -- SentrySDK.start(options: fixture.getOptions()) + + // -- Assert -- assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL) } } func testData_WritingWithOption_Tracking() { + // -- Arrange -- let expectedSpanCount: Int if #available(iOS 18, macOS 15, tvOS 18, *) { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 @@ -112,7 +126,11 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } else { expectedSpanCount = 1 } + + // -- Act -- SentrySDK.start(options: fixture.getOptions()) + + // -- Assert -- assertSpans(expectedSpanCount, "file.write") { try? fixture.data.write(to: fixture.fileURL, options: .atomic) } @@ -125,9 +143,9 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } - SentrySDK.start(options: fixture.getOptions()) - + // -- Act & Assert -- + SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { try? fixture.data.writeWithSentryTracing(to: fixture.fileURL) } @@ -135,44 +153,55 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func testDataExtension_WritingWithOption_Tracking() throws { + // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15. // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { - try? fixture.data - .writeWithSentryTracing(to: fixture.fileURL, options: .atomic) + try? fixture.data.writeWithSentryTracing(to: fixture.fileURL, options: .atomic) } fixture.assertDataWritten(toUrl: fixture.fileURL) } func test_ReadingTrackingDisabled_forIOOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_forSwizzlingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableSwizzling: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_forAutoPerformanceTrackingOption() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(enableAutoPerformanceTracing: false)) - + + // -- Assert -- assertWriteWithNoSpans() } func test_ReadingTrackingDisabled_TracingDisabled() { + // -- Act -- SentrySDK.start(options: fixture.getOptions(tracesSampleRate: 0)) - + + // -- Assert -- assertWriteWithNoSpans() } func testData_ReadingURL_Tracking() { + // -- Arrange -- let expectedSpanCount: Int if #available(iOS 18.0, macOS 15.0, tvOS 18.0, *) { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 15 @@ -182,6 +211,8 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } else { expectedSpanCount = 1 } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) assertSpans(expectedSpanCount, "file.read") { let _ = try? Data(contentsOf: fixture.fileURL) @@ -189,6 +220,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } func testData_ReadingURLWithOption_Tracking() throws { + // -- Arrange -- let expectedSpanCount: Int if #available(iOS 18.0, macOS 15.0, tvOS 18.0, *) { // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18 @@ -198,70 +230,85 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } else { expectedSpanCount = 1 } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(expectedSpanCount, "file.read") { - let data = try? Data(contentsOf: fixture.fileURL, options: .uncached) - XCTAssertEqual(data?.count, fixture.data.count) + let data = assertSpans(expectedSpanCount, "file.read") { + try? Data(contentsOf: fixture.fileURL, options: .uncached) } + XCTAssertEqual(data?.count, fixture.data.count) } func testDataExtension_ReadingURL_Tracking() throws { + // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL) - XCTAssertEqual(data?.count, fixture.data.count) + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL) } + XCTAssertEqual(data?.count, fixture.data.count) } func testDataExtension_ReadingURLWithOption_Tracking() throws { + // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } + + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL, options: .uncached) - XCTAssertEqual(data?.count, fixture.data.count) + let data = assertSpans(1, "file.read") { + try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL, options: .uncached) } + XCTAssertEqual(data?.count, fixture.data.count) } func test_ReadingFile_Tracking() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = NSData(contentsOfFile: fixture.filePath) - XCTAssertEqual(data?.count, fixture.data.count) + + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + NSData(contentsOfFile: fixture.filePath) } + XCTAssertEqual(data?.count, fixture.data.count) } func test_ReadingFileWithOptions_Tracking() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - assertSpans(1, "file.read") { - let data = try? NSData(contentsOfFile: fixture.filePath, options: .uncached) - XCTAssertEqual(data?.count, fixture.data.count) + + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? NSData(contentsOfFile: fixture.filePath, options: .uncached) } + XCTAssertEqual(data?.count, fixture.data.count) } func test_ReadingBigFile() { + // -- Arrange -- SentrySDK.start(options: fixture.getOptions()) - guard let jsonFile = getBigFilePath() else { XCTFail("Could not open Resource") return } - - assertSpans(1, "file.read") { - let data = try? NSData(contentsOfFile: jsonFile, options: .uncached) - XCTAssertEqual(data?.count, 341_431) + + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? NSData(contentsOfFile: jsonFile, options: .uncached) } + XCTAssertEqual(data?.count, 341_431) } func test_WritingBigFile() { + // -- Arrange -- guard let jsonFile = getBigFilePath() else { XCTFail("Could not open Resource") return @@ -273,14 +320,13 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } SentrySDK.start(options: fixture.getOptions()) - + + // -- Act & Assert -- assertSpans(1, "file.write") { try? data.write(to: fixture.fileURL, options: .atomic) - - let size = try? fixture.fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 - - XCTAssertEqual(size, 341_431) } + let size = try? fixture.fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 + XCTAssertEqual(size, 341_431) } func getBigFilePath() -> String? { @@ -323,28 +369,30 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { } } - private func assertSpans( + private func assertSpans( _ spansCount: Int, _ operation: String, _ description: String = "TestFile", - _ block: () -> Void, + _ block: () -> ReturnValue, file: StaticString = #file, line: UInt = #line - ) { + ) -> ReturnValue { let parentTransaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) - block() - + let result = block() + let childrenSelector = NSSelectorFromString("children") guard let children = parentTransaction.perform(childrenSelector).takeUnretainedValue() as? [Span] else { XCTFail("Did not found children property from transaction.", file: file, line: line) - return + return result } XCTAssertEqual(children.count, spansCount, "Actual span count is not equal to expected count", file: file, line: line) if let first = children.first { XCTAssertEqual(first.operation, operation, "Operation for span is not equal to expected operation", file: file, line: line) } + + return result } } diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index 9067236d15e..76aac408a58 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -4,7 +4,6 @@ #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" -#import "SentrySpanOperations.h" #import "SentrySwizzle.h" #import "SentryThreadInspector.h" #import "SentryTracer.h" From f29de608b4839b9dff7d50c1781e367d53439a50 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 13 Jan 2025 15:20:31 +0100 Subject: [PATCH 22/38] remove unused imported headers --- Sources/Sentry/include/SentryPrivate.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 0640194a491..dcce30824e4 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -9,17 +9,11 @@ // Headers that also import SentryDefines should be at the end of this list // otherwise it wont compile -#import "SentryByteCountFormatter.h" -#import "SentryClient+Private.h" #import "SentryDateUtil.h" #import "SentryDisplayLinkWrapper.h" #import "SentryFileIOTracker.h" -#import "SentryFileManager.h" #import "SentryLevelHelper.h" #import "SentryLogC.h" #import "SentryRandom.h" -#import "SentrySDK+Private.h" #import "SentrySdkInfo.h" #import "SentrySession.h" -#import "SentrySpanOperations.h" -#import "SentryTraceOrigins.h" From 89e13c0893a8bb40c2babdc1fd8b1e80e2ba62f4 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 14 Jan 2025 09:51:23 +0100 Subject: [PATCH 23/38] add more verbose xcframework build logs --- Makefile | 2 +- scripts/build-xcframework.sh | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index dc57858c12c..bf4a33ca047 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ analyze: # For more info check out: https://github.com/Carthage/Carthage/releases/tag/0.38.0 build-xcframework: @echo "--> Carthage: creating Sentry xcframework" - ./scripts/build-xcframework.sh > build-xcframework.log + ./scripts/build-xcframework.sh | tee build-xcframework.log # use ditto here to avoid clobbering symlinks which exist in macOS frameworks ditto -c -k -X --rsrc --keepParent Carthage/Sentry.xcframework Carthage/Sentry.xcframework.zip ditto -c -k -X --rsrc --keepParent Carthage/Sentry-Dynamic.xcframework Carthage/Sentry-Dynamic.xcframework.zip diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh index f97db64a06f..1505bcf5c12 100755 --- a/scripts/build-xcframework.sh +++ b/scripts/build-xcframework.sh @@ -46,7 +46,7 @@ generate_xcframework() { OTHER_LDFLAGS="-Wl,-make_mergeable" fi - xcodebuild archive \ + set -o pipefail && NSUnbufferedIO=YES xcodebuild archive \ -project Sentry.xcodeproj/ \ -scheme "$scheme" \ -configuration "$resolved_configuration" \ @@ -59,7 +59,7 @@ generate_xcframework() { MACH_O_TYPE="$MACH_O_TYPE" \ ENABLE_CODE_COVERAGE=NO \ GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS" \ - OTHER_LDFLAGS="$OTHER_LDFLAGS" + OTHER_LDFLAGS="$OTHER_LDFLAGS" 2>&1 | xcbeautify --preserve-unbeautified createxcframework+="-framework Carthage/archive/${scheme}${suffix}/${sdk}.xcarchive/Products/Library/Frameworks/${resolved_product_name}.framework " @@ -91,7 +91,7 @@ generate_xcframework() { if [ "$args" != "iOSOnly" ]; then #Create framework for mac catalyst - xcodebuild \ + set -o pipefail && NSUnbufferedIO=YES xcodebuild \ -project Sentry.xcodeproj/ \ -scheme "$scheme" \ -configuration "$resolved_configuration" \ @@ -105,7 +105,7 @@ generate_xcframework() { SUPPORTS_MACCATALYST=YES \ ENABLE_CODE_COVERAGE=NO \ GCC_GENERATE_DEBUGGING_SYMBOLS="$GCC_GENERATE_DEBUGGING_SYMBOLS" \ - OTHER_LDFLAGS="$OTHER_LDFLAGS" + OTHER_LDFLAGS="$OTHER_LDFLAGS" 2>&1 | xcbeautify --preserve-unbeautified if [ "$MACH_O_TYPE" = "staticlib" ]; then local infoPlist="Carthage/DerivedData/Build/Products/$resolved_configuration-maccatalyst/${scheme}.framework/Resources/Info.plist" From 3828edd3a8ab55b7d4ae4d83bafa0e605922c02f Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 14 Jan 2025 10:46:38 +0100 Subject: [PATCH 24/38] refactor measure methods to Swift --- Sentry.xcodeproj/project.pbxproj | 14 +++++- Sources/Sentry/include/SentryFileIOTracker.h | 9 ++++ .../Performance/IO}/Data+SentryTracing.swift | 32 +++++-------- .../IO/SentryFileIOTracker+SwiftHelpers.swift | 35 ++++++++++++++ ...SentryFileIOTrackingIntegrationTests.swift | 46 +++++++++++++++++-- 5 files changed, 110 insertions(+), 26 deletions(-) rename Sources/Swift/{Helper => Integrations/Performance/IO}/Data+SentryTracing.swift (63%) create mode 100644 Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d699386d712..e7addb4dbc9 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,6 +789,7 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; + D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; @@ -1881,6 +1882,7 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; @@ -2144,7 +2146,6 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( - D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */, @@ -3665,6 +3666,15 @@ name = Transaction; sourceTree = ""; }; + D468C0602D36699700964230 /* IO */ = { + isa = PBXGroup; + children = ( + D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */, + ); + path = IO; + sourceTree = ""; + }; D48724D92D35258A005DE483 /* Transactions */ = { isa = PBXGroup; children = ( @@ -3870,6 +3880,7 @@ D8739CF72BECFF92007D2F66 /* Performance */ = { isa = PBXGroup; children = ( + D468C0602D36699700964230 /* IO */, 6294287F2CB3BF4E002C454C /* SwizzleClassNameExclude.swift */, D8739CF82BECFFB5007D2F66 /* SentryTransactionNameSource.swift */, ); @@ -4829,6 +4840,7 @@ D84D2CC32C29AD120011AF8A /* SentrySessionReplay.swift in Sources */, 849B8F9B2C6E906900148E1F /* SentryUserFeedbackIntegrationDriver.swift in Sources */, 63FE70DF20DA4C1000CDBAE8 /* SentryCrashMonitorType.c in Sources */, + D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */, 7BF9EF7E2722B91F00B5BBEF /* SentryDefaultObjCRuntimeWrapper.m in Sources */, 7BC3936E25B1AB72004F03D3 /* SentryLevelMapper.m in Sources */, 6304360B1EC0595B00C4D3FA /* SentryNSDataUtils.m in Sources */, diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index 833c9cbdf22..9866535a87a 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -91,6 +91,15 @@ SENTRY_NO_INIT method:(BOOL (^)(NSString *, NSData *, NSDictionary *))method; +// MARK: - Internal Methods available for Swift Extension + +- (nullable id)startTrackingReadingFilePath:(NSString *)path origin:(NSString *)origin; +- (nullable id)startTrackingWritingNSData:(NSData *)data + filePath:(NSString *)path + origin:(NSString *)origin; +- (void)finishTrackingNSData:(NSData *)data span:(id)span; +- (void)endTrackingFile; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift similarity index 63% rename from Sources/Swift/Helper/Data+SentryTracing.swift rename to Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift index aa37e7b2c4f..bb79f3b5efb 100644 --- a/Sources/Swift/Helper/Data+SentryTracing.swift +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -14,17 +14,14 @@ public extension Data { /// - throws: An error in the Cocoa domain, if `url` cannot be read. init(contentsOfUrlWithSentryTracing url: URL, options: Data.ReadingOptions = []) throws { let tracker = SentryFileIOTracker.sharedInstance() - // `Data` is bridged to `NSData`, therefore we can use `measureNSData` to track the operation. + // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. + // Therefore we do not use the `measureNSData` method. self = try tracker - .measureNSData( + .measureReadingData( from: url, - origin: SentryTraceOrigin.manualData) { url, options, errorPtr in - do { - return try Data(contentsOf: url, options: options) - } catch { - errorPtr?.pointee = error as NSError - return nil - } + options: options, + origin: SentryTraceOrigin.manualData) { url, options in + try Data(contentsOf: url, options: options) } } @@ -35,20 +32,15 @@ public extension Data { /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. func writeWithSentryTracing(to url: URL, options: Data.WritingOptions = []) throws { let tracker = SentryFileIOTracker.sharedInstance() - // `Data` is bridged to `NSData`, therefore we can use `measure` to track the operation + // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. + // Therefore we do not use the `measureNSData` method. try tracker - .measure( + .measureWritingData( self, - writeTo: url, + to: url, options: options, - origin: SentryTraceOrigin.manualData) { url, options, errorPtr in - do { - try self.write(to: url, options: options) - return true - } catch { - errorPtr?.pointee = error as NSError - return false - } + origin: SentryTraceOrigin.manualData) { data, url, options in + try data.write(to: url, options: options) } } } diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift new file mode 100644 index 00000000000..6f7448a6026 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -0,0 +1,35 @@ +@_implementationOnly import _SentryPrivate + +extension SentryFileIOTracker { + func measureReadingData(from url: URL, options: Data.ReadingOptions, origin: String, method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data) throws -> Data { + guard url.scheme == NSURLFileScheme else { + return try method(url, options) + } + let span = self.startTrackingReadingFilePath(url.path, origin: origin) + defer { + self.endTrackingFile() + } + let result = try method(url, options) + if let span = span { + self.finishTrackingNSData(result, span: span) + } + return result + } + + func measureWritingData( + _ data: Data, + to url: URL, + options: Data.WritingOptions, + origin: String, + method: (_ data: Data, _ url: URL, _ options: Data.WritingOptions) throws -> Void + ) throws { + guard url.scheme == NSURLFileScheme else { + return try method(data, url, options) + } + let span = self.startTrackingWriting(data, filePath: url.path, origin: origin) + try method(data, url, options) + if let span = span { + self.finishTrackingNSData(data, span: span) + } + } +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index 7c020e80381..f3a7d1fb032 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -10,7 +10,9 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { let filePath: String! let fileURL: URL! let fileDirectory: URL! - + + let invalidFileURL = URL(fileURLWithPath: "/path/to/some/file") + func getOptions(enableAutoPerformanceTracing: Bool = true, enableFileIOTracing: Bool = true, enableSwizzling: Bool = true, tracesSampleRate: NSNumber = 1) -> Options { let result = Options() result.enableAutoPerformanceTracing = enableAutoPerformanceTracing @@ -239,7 +241,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) } - func testDataExtension_ReadingURL_Tracking() throws { + func testDataExtension_ReadingURL_fileExists_shouldBeTraced() throws { // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. // Therefore, the extension method is only tested with these OS versions. @@ -254,20 +256,54 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { XCTAssertEqual(data?.count, fixture.data.count) } - func testDataExtension_ReadingURLWithOption_Tracking() throws { + func testDataExtension_ReadingURL_fileNotFound_shouldStillBeTraced() throws { // -- Arrange -- // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. // Therefore, the extension method is only tested with these OS versions. guard #available(iOS 18, macOS 15, tvOS 18, *) else { throw XCTSkip("Data+SentryTracing is not tested on this OS version") } - + SentrySDK.start(options: fixture.getOptions()) + // -- Act & Assert -- + let data = assertSpans(1, "file.read") { + try? Data(contentsOfUrlWithSentryTracing: fixture.invalidFileURL) + } + XCTAssertNil(data) + } + + func testDataExtension_ReadingURLWithOption_fileExists_shouldBeTraced() throws { + // -- Arrange -- + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. + // Therefore, the extension method is only tested with these OS versions. + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Data+SentryTracing is not tested on this OS version") + } + // -- Act & Assert -- SentrySDK.start(options: fixture.getOptions()) let data = assertSpans(1, "file.read") { try? Data(contentsOfUrlWithSentryTracing: fixture.fileURL, options: .uncached) } - XCTAssertEqual(data?.count, fixture.data.count) + XCTAssertEqual(data, fixture.data) + } + + func testDataExtension_ReadingURLWithOption_fileNotFound_shouldStillBeTraced() throws { + // -- Arrange -- + // Automatic tracking of Swift.Data is not available starting with iOS 18, macOS 15, tvOS 18. + // Therefore, the extension method is only tested with these OS versions. + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Data+SentryTracing is not tested on this OS version") + } + + // -- Act & Assert -- + SentrySDK.start(options: fixture.getOptions()) + let data = assertSpans(1, "file.read") { + try? Data( + contentsOfUrlWithSentryTracing: fixture.invalidFileURL, + options: .uncached + ) + } + XCTAssertNil(data) } func test_ReadingFile_Tracking() { From 444a139fa5f3fbb0769d6fae5274e759fddd7480 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 16 Jan 2025 12:58:11 +0100 Subject: [PATCH 25/38] applied feedback --- Sources/Sentry/SentryFileIOTracker.m | 5 ++--- .../Integrations/Performance/IO/Data+SentryTracing.swift | 1 - .../SentryTests/Transactions/SentrySpanOperationTests.swift | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 188d05f0570..9baf6f8f1cd 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -223,8 +223,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path return nil; } - SENTRY_LOG_DEBUG( - @"SentryFileIOTracker automatically started a new span with description: %@, operation: %@", + SENTRY_LOG_DEBUG(@"Automatically started a new span with description: %@, operation: %@", ioSpan.description, operation); [ioSpan setDataValue:path forKey:@"file.path"]; @@ -310,7 +309,7 @@ - (void)finishTrackingNSData:(NSData *)data span:(id)span [span setDataValue:[NSNumber numberWithUnsignedInteger:data.length] forKey:@"file.size"]; [span finish]; - SENTRY_LOG_DEBUG(@"SentryFileIOTracker automatically finished span %@", span.description); + SENTRY_LOG_DEBUG(@"Automatically finished span %@", span.description); } - (BOOL)ignoreFile:(NSString *)path diff --git a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift index bb79f3b5efb..93646d45def 100644 --- a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -4,7 +4,6 @@ /// /// - Note: Methods provided by this extension reflect the same functionality as the original `Data` methods, /// but they automatically track the operation with Sentry. -@available(iOS 18, macOS 15, tvOS 18, *) public extension Data { /// Initialize a `Data` with the contents of a `URL`, automatically tracking the operation with Sentry. diff --git a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift index f5050fc54bf..a3d10d1de1b 100644 --- a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift +++ b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift @@ -3,12 +3,10 @@ import XCTest class SentrySpanOperationTests: XCTestCase { - /// This test asserts that the constant matches the SDK specification. func testFileRead_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanOperation.fileRead, "file.read") } - /// This test asserts that the constant matches the SDK specification. func testFileWrite_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanOperation.fileWrite, "file.write") } From f1e481658a022548140303716f35f179b025783f Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 16 Jan 2025 17:09:37 +0100 Subject: [PATCH 26/38] add defer to finish tracking nsdata --- .../Performance/IO/SentryFileIOTracker+SwiftHelpers.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift index 6f7448a6026..edd48077685 100644 --- a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -27,9 +27,11 @@ extension SentryFileIOTracker { return try method(data, url, options) } let span = self.startTrackingWriting(data, filePath: url.path, origin: origin) - try method(data, url, options) - if let span = span { - self.finishTrackingNSData(data, span: span) + defer { + if let span = span { + self.finishTrackingNSData(data, span: span) + } } + try method(data, url, options) } } From 185097a3315a8aad26805132e7716db8d28bb4d9 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 23 Jan 2025 10:30:42 +0100 Subject: [PATCH 27/38] fix changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f6782ab9b..2f1d2e0ffec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Use strlcpy to save session replay info path (#4740) +### Features + +- Add manual file IO tracking methods for Swift.Data (#4605) + ## 8.44.0-beta.1 ### Fixes @@ -21,7 +25,6 @@ - SwiftUI time for initial display and time for full display (#4596) - Add protocol for custom screenName for UIViewControllers (#4646) -- Add manual file IO tracking methods for Swift.Data (#4605) - Allow hybrid SDK to set replay options tags information (#4710) - Add threshold to always log fatal logs (#4707) From 2ffd1205a4507377b341d8e4034d03ec9392e606 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 23 Jan 2025 15:22:16 +0100 Subject: [PATCH 28/38] fix duplicate spans --- .../LoremIpsumViewController.swift | 19 +- Sentry.xcodeproj/project.pbxproj | 14 +- Sources/Sentry/SentryFileIOTracker.m | 4 +- .../Performance/IO/Data+SentryTracing.swift | 40 +-- .../IO/SentryFileIOTracker+SwiftHelpers.swift | 3 + .../Transactions/SentryTraceOrigin.swift | 1 + .../DataSentryTracingIntegrationTests.swift | 228 ++++++++++++++++++ 7 files changed, 283 insertions(+), 26 deletions(-) create mode 100644 Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift index d74750bd347..76d5bcb45b6 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift @@ -11,12 +11,23 @@ class LoremIpsumViewController: UIViewController { let dispatchQueue = DispatchQueue(label: "LoremIpsumViewController") dispatchQueue.async { if let path = Bundle.main.path(forResource: "LoremIpsum", ofType: "txt") { - if let contents = FileManager.default.contents(atPath: path) { + if let contents = try? Data( + contentsOfUrlWithSentryTracing: URL(fileURLWithPath: path) + ) { DispatchQueue.main.async { self.textView.text = String(data: contents, encoding: .utf8) - - dispatchQueue.asyncAfter(deadline: .now() + 0.1) { - SentrySDK.reportFullyDisplayed() + + dispatchQueue.async { + let tempURL = FileManager.default.temporaryDirectory + .appendingPathComponent("output.txt") + try! contents + .writeWithSentryTracing( + to: tempURL, + options: .atomic + ) + dispatchQueue.asyncAfter(deadline: .now() + 0.1) { + SentrySDK.reportFullyDisplayed() + } } } } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d24fb6c16db..4e5afaec0cd 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,14 +789,15 @@ A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; - D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; - D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; + D48724E22D354D16005DE483 /* SentryTraceOriginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */; }; + D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; }; D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */; }; D4AF00212D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */; }; D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; + D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */; }; D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; @@ -1895,14 +1896,15 @@ A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; - D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; - D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; + D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOriginTests.swift; sourceTree = ""; }; + D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D4AF00202D2E92FD00F5F3D7 /* SentryNSFileManagerSwizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzling.m; sourceTree = ""; }; D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSFileManagerSwizzling.h; path = include/SentryNSFileManagerSwizzling.h; sourceTree = ""; }; D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.m; sourceTree = ""; }; + D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSentryTracingIntegrationTests.swift; sourceTree = ""; }; D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+SentryTracing.swift"; sourceTree = ""; }; D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashCTests.swift; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; @@ -3638,8 +3640,6 @@ 8ECC674325C23A1F000E2BF6 /* SentrySpanContext.m */, 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */, 8E4E7C7325DAAB49006AB9E2 /* SentrySpanProtocol.h */, - 7B3B83712833832B0001FDEB /* SentrySpanOperations.h */, - 622C08D729E546F4002571D4 /* SentryTraceOrigins.h */, 8E4E7C6C25DAAAFE006AB9E2 /* SentrySpan.h */, 84A789092C0E9F5800FF0803 /* SentrySpan+Private.h */, 8EC3AE7925CA23B600E7591A /* SentrySpan.m */, @@ -3910,6 +3910,7 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( + D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */, D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, @@ -5199,6 +5200,7 @@ 8E70B0FD25CB72BE002B3155 /* SentrySpanTests.swift in Sources */, 7BBD188F2448469A00427C76 /* HttpDateFormatter.swift in Sources */, 63FE720C20DA66EC00CDBAE8 /* SentryCrashMonitor_Tests.m in Sources */, + D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */, D855B3EA27D652C700BCED76 /* TestCoreDataStack.swift in Sources */, D8AE48C12C57B1550092A2A6 /* SentryLevelTests.swift in Sources */, 63FE721820DA66EC00CDBAE8 /* TestThread.m in Sources */, diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 695fd0f3889..b742d3aec15 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -212,7 +212,9 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path } __block id ioSpan; - [SentrySDK.currentHub.scope useSpan:^(id _Nullable span) { + SentryHub *hub = SentrySDK.currentHub; + SentryScope *scope = hub.scope; + [scope useSpan:^(id _Nullable span) { ioSpan = [span startChildWithOperation:operation description:[self transactionDescriptionForFile:path fileSize:size]]; diff --git a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift index 93646d45def..526bd9eaee5 100644 --- a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -15,13 +15,18 @@ public extension Data { let tracker = SentryFileIOTracker.sharedInstance() // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. // Therefore we do not use the `measureNSData` method. - self = try tracker - .measureReadingData( - from: url, - options: options, - origin: SentryTraceOrigin.manualData) { url, options in - try Data(contentsOf: url, options: options) - } + if #available(iOS 18, macOS 15, tvOS 18, *) { + self = try tracker + .measureReadingData( + from: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { url, options in + try Data(contentsOf: url, options: options) + } + } else { + SentryLog.debug("Data is traced automatically on this platform version.") + self = try Data(contentsOf: url, options: options) + } } /// Write the contents of the `Data` to a location, automatically tracking the operation with Sentry. @@ -33,13 +38,18 @@ public extension Data { let tracker = SentryFileIOTracker.sharedInstance() // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. // Therefore we do not use the `measureNSData` method. - try tracker - .measureWritingData( - self, - to: url, - options: options, - origin: SentryTraceOrigin.manualData) { data, url, options in - try data.write(to: url, options: options) - } + if #available(iOS 18, macOS 15, tvOS 18, *) { + try tracker + .measureWritingData( + self, + to: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { data, url, options in + try data.write(to: url, options: options) + } + } else { + SentryLog.debug("Data is traced automatically on this platform version.") + try self.write(to: url, options: options) + } } } diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift index edd48077685..e7a05706ebd 100644 --- a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -27,6 +27,9 @@ extension SentryFileIOTracker { return try method(data, url, options) } let span = self.startTrackingWriting(data, filePath: url.path, origin: origin) + defer { + self.endTrackingFile() + } defer { if let span = span { self.finishTrackingNSData(data, span: span) diff --git a/Sources/Swift/Transactions/SentryTraceOrigin.swift b/Sources/Swift/Transactions/SentryTraceOrigin.swift index 34725f51a19..3a8661bdc7a 100644 --- a/Sources/Swift/Transactions/SentryTraceOrigin.swift +++ b/Sources/Swift/Transactions/SentryTraceOrigin.swift @@ -11,5 +11,6 @@ class SentryTraceOrigin: NSObject { static let autoUITimeToDisplay = "auto.ui.time_to_display" static let autoUIViewController = "auto.ui.view_controller" static let manual = "manual" + static let manualFileData = "manual.file.data" static let manualUITimeToDisplay = "manual.ui.time_to_display" } diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift new file mode 100644 index 00000000000..efd06d5cdcc --- /dev/null +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -0,0 +1,228 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class DataSentryTracingIntegrationTests: XCTestCase { + private class Fixture { + + let data = "SOME DATA".data(using: .utf8)! + + var fileUrlToRead: URL! + var fileUrlToWrite: URL! + + init() {} + + func getSut(testName: String) throws { + let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-\(testName.hashValue.description)") + try! FileManager.default + .createDirectory(at: tempDir, withIntermediateDirectories: true) + + fileUrlToRead = tempDir.appendingPathComponent("file-to-read") + try data.write(to: fileUrlToRead) + + fileUrlToWrite = tempDir.appendingPathComponent("file-to-write") + + // Initialize the SDK after files are written, so preparations are not traced + SentrySDK.start { options in + options.enableSwizzling = true + options.enableAutoPerformanceTracing = true + options.enableFileIOTracing = true + options.tracesSampleRate = 1.0 + options.setIntegrations([SentryFileIOTrackingIntegration.self]) + } + } + + var invalidFileUrlToRead: URL { + URL(fileURLWithPath: "/dev/null") + } + + var invalidFileUrlToWrite: URL { + URL(fileURLWithPath: "/path/that/does/not/exist") + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + } + + override func tearDown() { + super.tearDown() +// clearTestState() + } + + // MARK: - Data.init(contentsOfUrlWithSentryTracing:) + + func testInitContentsOfUrlWithSentryTracing_dataBridgedToNSData_shouldNotTraceManually() throws { + // -- Arrange -- + if #available(iOS 18, macOS 15, tvOS 18, *) { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfUrlWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) + } + + func testInitContentsOfUrlWithSentryTracing_dataBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + if #available(iOS 18, macOS 15, tvOS 18, *) { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try Data(contentsOfUrlWithSentryTracing: fixture.invalidFileUrlToRead)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertEqual(span.data["file.size"] as? Int, 0) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) + } + + func testInitContentsOfUrlWithSentryTracing_dataNotBridgedToNSData_shouldNotTraceManually() throws { + // -- Arrange -- + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfUrlWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) + } + + func testInitContentsOfUrlWithSentryTracing_dataNotBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try Data(contentsOfUrlWithSentryTracing: fixture.invalidFileUrlToRead)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) + XCTAssertNil(parentTransaction.children.first?.data["file.size"]) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) + } + + // MARK: - Data.writeWithSentryTracing(to:) + + func testWriteWithSentryTracing_dataBridgedToNSData_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + if #available(iOS 18, macOS 15, tvOS 18, *) { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try fixture.data.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: .atomic) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToWrite.path) + + // Reading the written data will create a span, so do it after asserting the transaction + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, fixture.data) + } + + func testWriteWithSentryTracing_dataBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + if #available(iOS 18, macOS 15, tvOS 18, *) { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try fixture.data.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) + } + + func testWriteWithSentryTracing_dataNotBridgedToNSData_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try fixture.data.writeWithSentryTracing(to: fixture.fileUrlToWrite) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToWrite.path) + + // Reading the written data will create a span, so do it after asserting the transaction + let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) + XCTAssertEqual(writtenData, fixture.data) + } + + func testWriteWithSentryTracing_dataNotBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + guard #available(iOS 18, macOS 15, tvOS 18, *) else { + throw XCTSkip("Test is disabled for this OS version") + } + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try fixture.data.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) + } +} From 0ca0bcbd4e152f63f62011d53ae55439ab5068c1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 23 Jan 2025 15:28:57 +0100 Subject: [PATCH 29/38] revert change in sample --- .../LoremIpsumViewController.swift | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift index 76d5bcb45b6..d74750bd347 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift @@ -11,23 +11,12 @@ class LoremIpsumViewController: UIViewController { let dispatchQueue = DispatchQueue(label: "LoremIpsumViewController") dispatchQueue.async { if let path = Bundle.main.path(forResource: "LoremIpsum", ofType: "txt") { - if let contents = try? Data( - contentsOfUrlWithSentryTracing: URL(fileURLWithPath: path) - ) { + if let contents = FileManager.default.contents(atPath: path) { DispatchQueue.main.async { self.textView.text = String(data: contents, encoding: .utf8) - - dispatchQueue.async { - let tempURL = FileManager.default.temporaryDirectory - .appendingPathComponent("output.txt") - try! contents - .writeWithSentryTracing( - to: tempURL, - options: .atomic - ) - dispatchQueue.asyncAfter(deadline: .now() + 0.1) { - SentrySDK.reportFullyDisplayed() - } + + dispatchQueue.asyncAfter(deadline: .now() + 0.1) { + SentrySDK.reportFullyDisplayed() } } } From 15393b0bf5e7fb95f0a8c44bf420aaf35e3fa757 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 6 Feb 2025 11:36:09 +0100 Subject: [PATCH 30/38] Update Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift Co-authored-by: Philipp Hofmann --- .../Integrations/Performance/IO/Data+SentryTracing.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift index 526bd9eaee5..02d12fdb1e1 100644 --- a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -1,9 +1,9 @@ @_implementationOnly import _SentryPrivate -/// A `Data` extension that automatically tracks read and write operations with Sentry. +/// A `Data` extension that tracks read and write operations with Sentry. /// /// - Note: Methods provided by this extension reflect the same functionality as the original `Data` methods, -/// but they automatically track the operation with Sentry. +/// but they track the operation with Sentry. public extension Data { /// Initialize a `Data` with the contents of a `URL`, automatically tracking the operation with Sentry. From 5d688e6375b01ed2a6d7380c4bd8bba802f78b3b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 12 Feb 2025 13:34:57 +0100 Subject: [PATCH 31/38] wip --- CHANGELOG.md | 5 +- Sentry.xcodeproj/project.pbxproj | 12 +- Sources/Sentry/SentryFileIOTracker.m | 4 +- .../Sentry/SentryFileIOTrackingIntegration.m | 4 +- Sources/Sentry/SentryNSDataSwizzling.m | 5 + Sources/Sentry/SentryNSFileManagerSwizzling.m | 4 +- Sources/Sentry/include/SentryFileIOTracker.h | 4 + .../Performance/IO/Data+SentryTracing.swift | 79 +++++----- .../IO/FileManager+SentryTracing.swift | 140 ++++++++++++++++++ .../IO/SentryFileIOTracker+SwiftHelpers.swift | 129 +++++++++++++++- Sources/Swift/SentryExperimentalOptions.swift | 5 + .../Transactions/SentrySpanOperation.swift | 3 + .../DataSentryTracingIntegrationTests.swift | 109 +------------- 13 files changed, 341 insertions(+), 162 deletions(-) create mode 100644 Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 048ec3037a1..cdb06805e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - HTTP Breadcrumb level based on response status code (#4779) 4xx is warning, 5xx is error. +- Add manual file IO tracking methods for Swift.Data (#4605) ### Improvements @@ -52,10 +53,6 @@ - Convert constants SentrySpanOperation to Swift (#4718) - Convert constants SentryTraceOrigins to Swift (#4717) -### Features - -- Add manual file IO tracking methods for Swift.Data (#4605) - ## 8.44.0-beta.1 ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 22202d6798b..65796ff2668 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -787,8 +787,9 @@ A8AFFCD42907E0CA00967CD7 /* SentryRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */; }; A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; - D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; + D43647AB2D5CAA32001468E0 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */; }; + D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */; }; @@ -799,9 +800,9 @@ D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF00222D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h */; }; D4AF00252D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D4AF00242D2E93C400F5F3D7 /* SentryNSFileManagerSwizzlingTests.m */; }; D4C5F59A2D4249E6002A9BF6 /* DataSentryTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */; }; - D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4E3F35D2D4A864600F79E2B /* SentryNSDictionarySanitizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */; }; D4E3F35E2D4A877300F79E2B /* SentryNSDictionarySanitize+Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */; }; + D4EDF9842D0B2A210071E7B3 /* Data+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */; }; D4F2B5352D0C69D500649E42 /* SentryCrashCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F2B5342D0C69D100649E42 /* SentryCrashCTests.swift */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; @@ -1901,11 +1902,12 @@ A8AFFCD32907E0CA00967CD7 /* SentryRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRequestTests.swift; sourceTree = ""; }; A8F17B2D2901765900990B25 /* SentryRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryRequest.m; sourceTree = ""; }; A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; - D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; - D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D41909922D48FFF6002B83D0 /* SentryNSDictionarySanitize+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryNSDictionarySanitize+Tests.h"; sourceTree = ""; }; D41909942D490006002B83D0 /* SentryNSDictionarySanitize+Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryNSDictionarySanitize+Tests.m"; sourceTree = ""; }; + D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; + D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SentryTracing.swift"; sourceTree = ""; }; + D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperationTests.swift; sourceTree = ""; }; @@ -3705,6 +3707,7 @@ isa = PBXGroup; children = ( D4EDF9832D0B2A1D0071E7B3 /* Data+SentryTracing.swift */, + D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */, D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */, ); path = IO; @@ -4857,6 +4860,7 @@ 843FB3232D0CD04D00558F18 /* SentryUserAccess.m in Sources */, 63FE716720DA4C1100CDBAE8 /* SentryCrashCPU.c in Sources */, 63FE717320DA4C1100CDBAE8 /* SentryCrashC.c in Sources */, + D43647AB2D5CAA32001468E0 /* FileManager+SentryTracing.swift in Sources */, 63FE712120DA4C1000CDBAE8 /* SentryCrashSymbolicator.c in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index b742d3aec15..695fd0f3889 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -212,9 +212,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path } __block id ioSpan; - SentryHub *hub = SentrySDK.currentHub; - SentryScope *scope = hub.scope; - [scope useSpan:^(id _Nullable span) { + [SentrySDK.currentHub.scope useSpan:^(id _Nullable span) { ioSpan = [span startChildWithOperation:operation description:[self transactionDescriptionForFile:path fileSize:size]]; diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index d634e416fff..1ab9c2d38c0 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -30,8 +30,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options - (SentryIntegrationOption)integrationOptions { - return kIntegrationOptionEnableSwizzling | kIntegrationOptionIsTracingEnabled - | kIntegrationOptionEnableAutoPerformanceTracing | kIntegrationOptionEnableFileIOTracing; + return kIntegrationOptionIsTracingEnabled | kIntegrationOptionEnableAutoPerformanceTracing + | kIntegrationOptionEnableFileIOTracing; } - (void)uninstall diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 3ec1b0a83b1..63ba8db5cb2 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -23,6 +23,11 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * { self.tracker = tracker; + if (!options.enableSwizzling || !options.experimental.enableFileManagerSwizzling) { + SENTRY_LOG_DEBUG(@"Experimental auto-tracking of NSData is disabled") + return; + } + [SentryNSDataSwizzling swizzle]; } diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 234c27f94b5..5c988158832 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -23,8 +23,8 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * { self.tracker = tracker; - if (!options.experimental.enableFileManagerSwizzling) { - SENTRY_LOG_DEBUG(@"Experimental auto-tracking of FileManager is disabled") + if (!options.enableSwizzling || !options.experimental.enableFileManagerSwizzling) { + SENTRY_LOG_DEBUG(@"Experimental auto-tracking of NSFileManager is disabled") return; } diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index 61574781598..03c3cf3d42e 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -96,6 +96,10 @@ SENTRY_NO_INIT - (nullable id)startTrackingWritingNSData:(NSData *)data filePath:(NSString *)path origin:(NSString *)origin; +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation + size:(NSUInteger)size; - (void)finishTrackingNSData:(NSData *)data span:(id)span; - (void)endTrackingFile; diff --git a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift index 02d12fdb1e1..d050932e215 100644 --- a/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift +++ b/Sources/Swift/Integrations/Performance/IO/Data+SentryTracing.swift @@ -1,55 +1,52 @@ @_implementationOnly import _SentryPrivate -/// A `Data` extension that tracks read and write operations with Sentry. +/// A ``Data`` extension that tracks read and write operations with Sentry. /// -/// - Note: Methods provided by this extension reflect the same functionality as the original `Data` methods, -/// but they track the operation with Sentry. +/// - Note: Methods provided by this extension reflect the same functionality as the original ``Data`` methods, but they track the operation with Sentry. public extension Data { - /// Initialize a `Data` with the contents of a `URL`, automatically tracking the operation with Sentry. + // MARK: - Reading Data from a File + + /// Creates a data object from the data at the specified file URL, tracking the operation with Sentry. /// - /// - parameter url: The `URL` to read. - /// - parameter options: Options for the read operation. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if `url` cannot be read. + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - url: The location on disk of the data to read. + /// - options: The mask specifying the options to use when reading the data. For more information, see ``NSData.ReadingOptions``. + /// - Note: See ``Data.init(contentsOf:options:)`` for more information. init(contentsOfUrlWithSentryTracing url: URL, options: Data.ReadingOptions = []) throws { let tracker = SentryFileIOTracker.sharedInstance() - // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. - // Therefore we do not use the `measureNSData` method. - if #available(iOS 18, macOS 15, tvOS 18, *) { - self = try tracker - .measureReadingData( - from: url, - options: options, - origin: SentryTraceOrigin.manualFileData) { url, options in - try Data(contentsOf: url, options: options) - } - } else { - SentryLog.debug("Data is traced automatically on this platform version.") - self = try Data(contentsOf: url, options: options) - } + self = try tracker + .measureReadingData( + from: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { url, options in + try Data(contentsOf: url, options: options) + } } - /// Write the contents of the `Data` to a location, automatically tracking the operation with Sentry. - /// - /// - parameter url: The location to write the data into. - /// - parameter options: Options for writing the data. Default value is `[]`. - /// - throws: An error in the Cocoa domain, if there is an error writing to the `URL`. + // MARK: - Writing Data to a File + + /// Write the contents of the `Data` to a location, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - url: The location to write the data into. + /// - options: Options for writing the data. Default value is `[]`. + /// - Note: See ``Data.write(to:options:)`` for more information. func writeWithSentryTracing(to url: URL, options: Data.WritingOptions = []) throws { let tracker = SentryFileIOTracker.sharedInstance() - // Using the bridging of `Data` to `NSData` caused issues on older versions of macOS. - // Therefore we do not use the `measureNSData` method. - if #available(iOS 18, macOS 15, tvOS 18, *) { - try tracker - .measureWritingData( - self, - to: url, - options: options, - origin: SentryTraceOrigin.manualFileData) { data, url, options in - try data.write(to: url, options: options) - } - } else { - SentryLog.debug("Data is traced automatically on this platform version.") - try self.write(to: url, options: options) - } + try tracker + .measureWritingData( + self, + to: url, + options: options, + origin: SentryTraceOrigin.manualFileData) { data, url, options in + try data.write(to: url, options: options) + } } } diff --git a/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift b/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift new file mode 100644 index 00000000000..a95aa118101 --- /dev/null +++ b/Sources/Swift/Integrations/Performance/IO/FileManager+SentryTracing.swift @@ -0,0 +1,140 @@ +@_implementationOnly import _SentryPrivate +import Foundation + +/// A ``FileManager`` extension that tracks read and write operations with Sentry. +/// +/// - Note: Methods provided by this extension reflect the same functionality as the original ``FileManager`` methods, but they track the operation with Sentry. +public extension FileManager { + + // MARK: - Creating and Deleting Items + + /// Creates a file with the specified content and attributes at the given location, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - path: The path for the new file. + /// - data: A data object containing the contents of the new file. + /// - attr: A dictionary containing the attributes to associate with the new file. + /// You can use these attributes to set the owner and group numbers, file permissions, and modification date. + /// For a list of keys, see ``FileAttributeKey``. If you specify `nil` for attributes, the file is created with a set of default attributes. + /// - Returns: `true` if the operation was successful or if the item already exists, otherwise `false`. + /// - Note: See ``FileManager.createFile(atPath:contents:attributes:)`` for more information. + func createFileWithSentryTracing(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey: Any]? = nil) -> Bool { + let tracker = SentryFileIOTracker.sharedInstance() + return tracker + .measureCreatingFile( + atPath: path, + contents: data, + attributes: attr, + origin: SentryTraceOrigin.manualFileData) { path, data, attr in + self.createFile(atPath: path, contents: data, attributes: attr) + } + } + + /// Removes the file or directory at the specified URL, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameter url: A file URL specifying the file or directory to remove. + /// If the URL specifies a directory, the contents of that directory are recursively removed. + /// - Note: See ``FileManager.removeItem(at:)`` for more information. + func removeItemWithSentryTracing(at url: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureRemovingItem(at: url, origin: SentryTraceOrigin.manualFileData) { url in + try self.removeItem(at: url) + } + } + + /// Removes the file or directory at the specified path, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameter path: A path string indicating the file or directory to remove. + /// If the path specifies a directory, the contents of that directory are recursively removed. + /// - Note: See ``FileManager.removeItem(atPath:)`` for more information. + func removeItemWithSentryTracing(atPath path: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureRemovingItem(atPath: path, origin: SentryTraceOrigin.manualFileData) { path in + try self.removeItem(atPath: path) + } + } + + // MARK: - Moving and Copying Items + + /// Copies the file at the specified URL to a new location synchronously, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcURL: The file URL that identifies the file you want to copy. + /// The URL in this parameter must not be a file reference URL. + /// - dstURL: The URL at which to place the copy of `srcURL`. + /// The URL in this parameter must not be a file reference URL and must include the name of the file in its new location. + /// - Note: See ``FileManager.copyItem(at:to:)`` for more information. + func copyItemWithSentryTracing(at srcURL: URL, to dstURL: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureCopyingItem(at: srcURL, to: dstURL, origin: SentryTraceOrigin.manualFileData) { srcURL, dstURL in + try self.copyItem(at: srcURL, to: dstURL) + } + } + + /// Copies the item at the specified path to a new location synchronously, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcPath: The path to the file or directory you want to move. + /// - dstPath: The path at which to place the copy of `srcPath`. + /// This path must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.copyItem(atPath:toPath:)`` for more information. + func copyItemWithSentryTracing(at srcPath: String, to dstPath: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureCopyingItem(atPath: srcPath, toPath: dstPath, origin: SentryTraceOrigin.manualFileData) { srcPath, dstPath in + try self.copyItem(atPath: srcPath, toPath: dstPath) + } + } + + /// Moves the file or directory at the specified URL to a new location synchronously, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcURL: The file URL that identifies the file or directory you want to move. + /// The URL in this parameter must not be a file reference URL. + /// - dstURL: The new location for the item in `srcURL`. + /// The URL in this parameter must not be a file reference URL and must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.moveItem(at:to:)`` for more information. + func moveItemWithSentryTracing(at srcURL: URL, to dstURL: URL) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureMovingItem( + at: srcURL, + to: dstURL, + origin: SentryTraceOrigin.manualFileData) { srcURL, dstURL in + try self.moveItem(at: srcURL, to: dstURL) + } + } + + /// Moves the file or directory at the specified path to a new location synchronously, tracking the operation with Sentry. + /// + /// - Important: Using this method with auto-instrumentation for file operations enabled can lead to duplicate spans on older operating system versions. + /// It is recommended to use either automatic or manual instrumentation. You can disable automatic instrumentation by setting + /// `options.enableSwizzling` to `false` when initializing Sentry. + /// - Parameters: + /// - srcPath: The path to the file or directory you want to move. + /// - dstPath: The new path for the item in `srcPath`. + /// This path must include the name of the file or directory in its new location. + /// - Note: See ``FileManager.moveItem(atPath:toPath:)`` for more information. + func moveItemWithSentryTracing(at srcPath: String, to dstPath: String) throws { + let tracker = SentryFileIOTracker.sharedInstance() + try tracker.measureMovingItem(atPath: srcPath, toPath: dstPath, origin: SentryTraceOrigin.manualFileData) { srcPath, dstPath in + try self.moveItem(atPath: srcPath, toPath: dstPath) + } + } +} diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift index e7a05706ebd..ce4ef26fefb 100644 --- a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -1,7 +1,12 @@ @_implementationOnly import _SentryPrivate extension SentryFileIOTracker { - func measureReadingData(from url: URL, options: Data.ReadingOptions, origin: String, method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data) throws -> Data { + func measureReadingData( + from url: URL, + options: Data.ReadingOptions, + origin: String, + method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data + ) throws -> Data { guard url.scheme == NSURLFileScheme else { return try method(url, options) } @@ -37,4 +42,126 @@ extension SentryFileIOTracker { } try method(data, url, options) } + + func measureRemovingItem( + at url: URL, + origin: String, + method: (_ url: URL) throws -> Void + ) throws { + guard url.scheme == NSURLFileScheme else { + return try method(url) + } + let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileDelete, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(url) + } + + func measureRemovingItem( + atPath path: String, + origin: String, + method: (_ path: String) throws -> Void + ) throws { + let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileDelete, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(path) + } + + func measureCreatingFile( + atPath path: String, + contents data: Data?, + attributes attr: [FileAttributeKey: Any]?, + origin: String, + method: (_ path: String, _ data: Data?, _ attributes: [FileAttributeKey: Any]?) -> Bool + ) -> Bool { + let span = self.startTrackingWriting(data ?? Data(), filePath: path, origin: origin) + defer { + self.endTrackingFile() + } + defer { + if let span = span { + self.finishTrackingNSData(data ?? Data(), span: span) + } + } + return method(path, data, attr) + } + + func measureCopyingItem( + at srcUrl: URL, + to dstUrl: URL, + origin: String, + method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void + ) throws { + guard srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { + return try method(srcUrl, dstUrl) + } + let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileCopy, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(srcUrl, dstUrl) + } + + func measureCopyingItem( + atPath srcPath: String, + toPath dstPath: String, + origin: String, + method: (_ srcPath: String, _ dstPath: String) throws -> Void + ) throws { + let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileCopy, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(srcPath, dstPath) + } + + func measureMovingItem( + at srcUrl: URL, + to dstUrl: URL, + origin: String, + method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void + ) throws { + guard srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { + return try method(srcUrl, dstUrl) + } + let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileRename, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(srcUrl, dstUrl) + } + + func measureMovingItem( + atPath srcPath: String, + toPath dstPath: String, + origin: String, + method: (_ srcPath: String, _ dstPath: String) throws -> Void + ) throws { + let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileRename, size: 0) + defer { + self.endTrackingFile() + } + defer { + span?.finish() + } + try method(srcPath, dstPath) + } } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 7c35b175d1f..f2a7dfeb2d2 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -1,5 +1,10 @@ @objcMembers public class SentryExperimentalOptions: NSObject { + /** + * Enables swizzling of`NSData` to automatically track file operations. + */ + public var enableDataSwizzling = false + /** * Enables swizzling of`NSFileManager` to automatically track file operations. */ diff --git a/Sources/Swift/Transactions/SentrySpanOperation.swift b/Sources/Swift/Transactions/SentrySpanOperation.swift index 30ccf342ea0..d2c24e6c2fb 100644 --- a/Sources/Swift/Transactions/SentrySpanOperation.swift +++ b/Sources/Swift/Transactions/SentrySpanOperation.swift @@ -9,6 +9,9 @@ class SentrySpanOperation: NSObject { static let fileRead = "file.read" static let fileWrite = "file.write" + static let fileCopy = "file.copy" + static let fileRename = "file.rename" + static let fileDelete = "file.delete" static let networkRequestOperation = "http.client" diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index efd06d5cdcc..08ddd24be22 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -49,18 +49,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { fixture = Fixture() } - override func tearDown() { - super.tearDown() -// clearTestState() - } - // MARK: - Data.init(contentsOfUrlWithSentryTracing:) - func testInitContentsOfUrlWithSentryTracing_dataBridgedToNSData_shouldNotTraceManually() throws { + func testInitContentsOfUrlWithSentryTracing_shouldTraceManually() throws { // -- Arrange -- - if #available(iOS 18, macOS 15, tvOS 18, *) { - throw XCTSkip("Test is disabled for this OS version") - } let _ = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -78,11 +70,8 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) } - func testInitContentsOfUrlWithSentryTracing_dataBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + func testInitContentsOfUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- - if #available(iOS 18, macOS 15, tvOS 18, *) { - throw XCTSkip("Test is disabled for this OS version") - } let _ = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -97,54 +86,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) } - func testInitContentsOfUrlWithSentryTracing_dataNotBridgedToNSData_shouldNotTraceManually() throws { - // -- Arrange -- - guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("Test is disabled for this OS version") - } - let _ = try fixture.getSut(testName: self.name) - let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - - // -- Act -- - let data = try Data(contentsOfUrlWithSentryTracing: fixture.fileUrlToRead) - - // -- Assert -- - XCTAssertEqual(data, fixture.data) - - XCTAssertEqual(parentTransaction.children.count, 1) - let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) - XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) - XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) - } - - func testInitContentsOfUrlWithSentryTracing_dataNotBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { - // -- Arrange -- - guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("Test is disabled for this OS version") - } - let _ = try fixture.getSut(testName: self.name) - let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - - // -- Act & Assert -- - XCTAssertThrowsError(try Data(contentsOfUrlWithSentryTracing: fixture.invalidFileUrlToRead)) - - XCTAssertEqual(parentTransaction.children.count, 1) - let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) - XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) - XCTAssertNil(parentTransaction.children.first?.data["file.size"]) - XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) - } - // MARK: - Data.writeWithSentryTracing(to:) - func testWriteWithSentryTracing_dataBridgedToNSData_shouldNotTraceManuallyWithErrorRethrow() throws { + func testWriteWithSentryTracing_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- - if #available(iOS 18, macOS 15, tvOS 18, *) { - throw XCTSkip("Test is disabled for this OS version") - } let _ = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -164,11 +109,8 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(writtenData, fixture.data) } - func testWriteWithSentryTracing_dataBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { + func testWriteWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- - if #available(iOS 18, macOS 15, tvOS 18, *) { - throw XCTSkip("Test is disabled for this OS version") - } let _ = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -182,47 +124,4 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) } - - func testWriteWithSentryTracing_dataNotBridgedToNSData_shouldNotTraceManuallyWithErrorRethrow() throws { - // -- Arrange -- - guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("Test is disabled for this OS version") - } - let _ = try fixture.getSut(testName: self.name) - let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - - // -- Act -- - try fixture.data.writeWithSentryTracing(to: fixture.fileUrlToWrite) - - // -- Assert -- - XCTAssertEqual(parentTransaction.children.count, 1) - let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) - XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) - XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToWrite.path) - - // Reading the written data will create a span, so do it after asserting the transaction - let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) - XCTAssertEqual(writtenData, fixture.data) - } - - func testWriteWithSentryTracing_dataNotBridgedToNSDataThrowsError_shouldNotTraceManuallyWithErrorRethrow() throws { - // -- Arrange -- - guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("Test is disabled for this OS version") - } - let _ = try fixture.getSut(testName: self.name) - let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - - // -- Act & Assert -- - XCTAssertThrowsError(try fixture.data.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) - - XCTAssertEqual(parentTransaction.children.count, 1) - let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) - XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) - XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) - } } From c1a15fec6ecee6abe33da65953e0c2a5162d7a6a Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 11:28:29 +0100 Subject: [PATCH 32/38] wip --- CHANGELOG.md | 2 +- Sentry.xcodeproj/project.pbxproj | 12 + .../xcschemes/SentryTests.xcscheme | 3 +- Sources/Sentry/SentryFileIOTracker.m | 28 +- Sources/Sentry/SentryNSDataSwizzling.m | 2 +- Sources/Sentry/include/SentryFileIOTracker.h | 7 +- .../IO/SentryFileIOTracker+SwiftHelpers.swift | 91 ++--- Sources/Swift/SentryExperimentalOptions.swift | 4 +- .../Swift/Transactions/SentrySpanKey.swift | 7 + .../DataSentryTracingIntegrationTests.swift | 16 +- .../FileManagerTracingIntegrationTests.swift | 354 ++++++++++++++++++ .../Transactions/SentrySpanKeyTests.swift | 8 + scripts/.clang-format-version | 2 +- scripts/.swiftlint-version | 2 +- 14 files changed, 475 insertions(+), 63 deletions(-) create mode 100644 Sources/Swift/Transactions/SentrySpanKey.swift create mode 100644 Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift create mode 100644 Tests/SentryTests/Transactions/SentrySpanKeyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index cdb06805e9c..00c04910c1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features - HTTP Breadcrumb level based on response status code (#4779) 4xx is warning, 5xx is error. -- Add manual file IO tracking methods for Swift.Data (#4605) +- Add manual file IO tracking methods for Swift.Data and Swift.FileManager (#4605) ### Improvements diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 65796ff2668..ecf7e6b128e 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -789,6 +789,9 @@ A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D42E48572D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */; }; D43647AB2D5CAA32001468E0 /* FileManager+SentryTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */; }; + D43647EF2D5CF9E3001468E0 /* SentrySpanKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647EE2D5CF9DC001468E0 /* SentrySpanKey.swift */; }; + D43647F12D5CFB71001468E0 /* SentrySpanKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */; }; + D43647F32D5CFBC7001468E0 /* FileManagerTracingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.swift */; }; D468C0622D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */; }; D48724DB2D352597005DE483 /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */; }; D48724DD2D354939005DE483 /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48724DC2D354934005DE483 /* SentrySpanOperation.swift */; }; @@ -1907,6 +1910,9 @@ D42E48562D48DF1600D251BC /* SentryBuildAppStartSpansTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBuildAppStartSpansTests.swift; sourceTree = ""; }; D42E48582D48FC8F00D251BC /* SentryNSDictionarySanitizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSDictionarySanitizeTests.swift; sourceTree = ""; }; D43647AA2D5CAA32001468E0 /* FileManager+SentryTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SentryTracing.swift"; sourceTree = ""; }; + D43647EE2D5CF9DC001468E0 /* SentrySpanKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanKey.swift; sourceTree = ""; }; + D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentrySpanKeyTests.swift; sourceTree = ""; }; + D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerTracingIntegrationTests.swift; sourceTree = ""; }; D468C0612D3669A200964230 /* SentryFileIOTracker+SwiftHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SentryFileIOTracker+SwiftHelpers.swift"; sourceTree = ""; }; D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = ""; }; D48724DC2D354934005DE483 /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = ""; }; @@ -3718,6 +3724,7 @@ children = ( D48724DA2D352591005DE483 /* SentryTraceOrigin.swift */, D48724DC2D354934005DE483 /* SentrySpanOperation.swift */, + D43647EE2D5CF9DC001468E0 /* SentrySpanKey.swift */, ); path = Transactions; sourceTree = ""; @@ -3727,6 +3734,7 @@ children = ( D48724E12D354D16005DE483 /* SentryTraceOriginTests.swift */, D48724DF2D3549C6005DE483 /* SentrySpanOperationTests.swift */, + D43647F02D5CFB71001468E0 /* SentrySpanKeyTests.swift */, ); path = Transactions; sourceTree = ""; @@ -3943,6 +3951,7 @@ D875ED09276CC83200422FAC /* IO */ = { isa = PBXGroup; children = ( + D43647F22D5CFBC2001468E0 /* FileManagerTracingIntegrationTests.swift */, D4C5F5992D4249E0002A9BF6 /* DataSentryTracingIntegrationTests.swift */, D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, @@ -4879,6 +4888,7 @@ D82859432C3E753C009A28AA /* SentrySessionReplaySyncC.c in Sources */, D833D57C2D10784800961E7A /* SentryRRWebOptionsEvent.swift in Sources */, 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */, + D43647EF2D5CF9E3001468E0 /* SentrySpanKey.swift in Sources */, 7B56D73324616D9500B842DA /* SentryConcurrentRateLimitsDictionary.m in Sources */, 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, 0A80E433291017C300095219 /* SentryWatchdogTerminationScopeObserver.m in Sources */, @@ -5256,6 +5266,7 @@ 8F73BC312B02B87E00C3CEF4 /* SentryInstallationTests.swift in Sources */, 7B569E002590EEF600B653FC /* SentryScope+Equality.m in Sources */, D8BFE37929A76666002E73F3 /* SentryTimeToDisplayTrackerTest.swift in Sources */, + D43647F32D5CFBC7001468E0 /* FileManagerTracingIntegrationTests.swift in Sources */, D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */, 7BF536D424BEF255004FA6A2 /* SentryAssertions.swift in Sources */, 7BC6EC14255C415E0059822A /* SentryExceptionTests.swift in Sources */, @@ -5307,6 +5318,7 @@ 7BF9EF7A2722B58900B5BBEF /* SentrySubClassFinderTests.swift in Sources */, D48724E02D3549CA005DE483 /* SentrySpanOperationTests.swift in Sources */, 7B59398224AB47650003AAD2 /* SentrySessionTrackerTests.swift in Sources */, + D43647F12D5CFB71001468E0 /* SentrySpanKeyTests.swift in Sources */, 7B05A61824A4D14A00EF211D /* SentrySessionGeneratorTests.swift in Sources */, D8CB742B294B1DD000A5F964 /* SentryUIApplicationTests.swift in Sources */, 63FE720920DA66EC00CDBAE8 /* XCTestCase+SentryCrash.m in Sources */, diff --git a/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme b/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme index 52b04c2e68b..26fdfd4936c 100644 --- a/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme +++ b/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme @@ -10,7 +10,8 @@ buildConfiguration = "Test" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 695fd0f3889..5e82a256b01 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -123,7 +123,9 @@ - (NSData *)measureNSDataFromFile:(NSString *)path origin:(NSString *)origin method:(NSData * (^)(NSString *))method { - id span = [self startTrackingReadingFilePath:path origin:origin]; + id span = [self startTrackingReadingFilePath:path + origin:origin + operation:SentrySpanOperation.fileRead]; NSData *result = method(path); @@ -141,7 +143,9 @@ - (NSData *)measureNSDataFromFile:(NSString *)path error:(NSError **)error method:(NSData * (^)(NSString *, NSDataReadingOptions, NSError **))method { - id span = [self startTrackingReadingFilePath:path origin:origin]; + id span = [self startTrackingReadingFilePath:path + origin:origin + operation:SentrySpanOperation.fileRead]; NSData *result = method(path, readOptionsMask, error); @@ -166,7 +170,9 @@ - (NSData *)measureNSDataFromURL:(NSURL *)url if (![url.scheme isEqualToString:NSURLFileScheme]) return method(url, readOptionsMask, error); - id span = [self startTrackingReadingFilePath:url.path origin:origin]; + id span = [self startTrackingReadingFilePath:url.path + origin:origin + operation:SentrySpanOperation.fileRead]; NSData *result = method(url, readOptionsMask, error); @@ -196,6 +202,13 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path return result; } +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation +{ + return [self spanForPath:path origin:origin operation:operation size:0]; +} + - (nullable id)spanForPath:(NSString *)path origin:(NSString *)origin operation:(NSString *)operation @@ -217,6 +230,9 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path description:[self transactionDescriptionForFile:path fileSize:size]]; ioSpan.origin = origin; + if (size > 0) { + [ioSpan setDataValue:[NSNumber numberWithUnsignedInteger:size] forKey:@"file.size"]; + } }]; if (ioSpan == nil) { @@ -274,7 +290,9 @@ - (void)mainThreadExtraInfo:(id)span size:data.length]; } -- (nullable id)startTrackingReadingFilePath:(NSString *)path origin:(NSString *)origin +- (nullable id)startTrackingReadingFilePath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation { // Some iOS versions nest constructors calls. This counter help us avoid create more than one // span for the same operation. @@ -286,7 +304,7 @@ - (void)mainThreadExtraInfo:(id)span if (count) return nil; - return [self spanForPath:path origin:origin operation:SentrySpanOperation.fileRead size:0]; + return [self spanForPath:path origin:origin operation:operation size:0]; } - (void)endTrackingFile diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 63ba8db5cb2..191a8383a28 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -23,7 +23,7 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * { self.tracker = tracker; - if (!options.enableSwizzling || !options.experimental.enableFileManagerSwizzling) { + if (!options.enableSwizzling || options.experimental.disableDataSwizzling) { SENTRY_LOG_DEBUG(@"Experimental auto-tracking of NSData is disabled") return; } diff --git a/Sources/Sentry/include/SentryFileIOTracker.h b/Sources/Sentry/include/SentryFileIOTracker.h index 03c3cf3d42e..864a2ae8c3b 100644 --- a/Sources/Sentry/include/SentryFileIOTracker.h +++ b/Sources/Sentry/include/SentryFileIOTracker.h @@ -92,10 +92,15 @@ SENTRY_NO_INIT // MARK: - Internal Methods available for Swift Extension -- (nullable id)startTrackingReadingFilePath:(NSString *)path origin:(NSString *)origin; +- (nullable id)startTrackingReadingFilePath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation; - (nullable id)startTrackingWritingNSData:(NSData *)data filePath:(NSString *)path origin:(NSString *)origin; +- (nullable id)spanForPath:(NSString *)path + origin:(NSString *)origin + operation:(NSString *)operation; - (nullable id)spanForPath:(NSString *)path origin:(NSString *)origin operation:(NSString *)operation diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift index ce4ef26fefb..729416d2cf4 100644 --- a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -7,18 +7,21 @@ extension SentryFileIOTracker { origin: String, method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data ) throws -> Data { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. guard url.scheme == NSURLFileScheme else { return try method(url, options) } - let span = self.startTrackingReadingFilePath(url.path, origin: origin) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileRead) else { + return try method(url, options) } - let result = try method(url, options) - if let span = span { - self.finishTrackingNSData(result, span: span) + defer { + span.finish() } - return result + let data = try method(url, options) + span.setData(value: data.count, key: SentrySpanKey.fileSize) + return data } func measureWritingData( @@ -28,17 +31,17 @@ extension SentryFileIOTracker { origin: String, method: (_ data: Data, _ url: URL, _ options: Data.WritingOptions) throws -> Void ) throws { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. guard url.scheme == NSURLFileScheme else { return try method(data, url, options) } - let span = self.startTrackingWriting(data, filePath: url.path, origin: origin) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileWrite, size: UInt(data.count)) else { + return try method(data, url, options) } defer { - if let span = span { - self.finishTrackingNSData(data, span: span) - } + span.finish() } try method(data, url, options) } @@ -48,15 +51,17 @@ extension SentryFileIOTracker { origin: String, method: (_ url: URL) throws -> Void ) throws { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. guard url.scheme == NSURLFileScheme else { return try method(url) } - let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileDelete, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: url.path, origin: origin, operation: SentrySpanOperation.fileDelete) else { + return try method(url) } defer { - span?.finish() + span.finish() } try method(url) } @@ -66,12 +71,11 @@ extension SentryFileIOTracker { origin: String, method: (_ path: String) throws -> Void ) throws { - let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileDelete, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileDelete) else { + return try method(path) } defer { - span?.finish() + span.finish() } try method(path) } @@ -83,14 +87,15 @@ extension SentryFileIOTracker { origin: String, method: (_ path: String, _ data: Data?, _ attributes: [FileAttributeKey: Any]?) -> Bool ) -> Bool { - let span = self.startTrackingWriting(data ?? Data(), filePath: path, origin: origin) - defer { - self.endTrackingFile() + let size = UInt(data?.count ?? 0) + guard let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileWrite, size: size) else { + return method(path, data, attr) } defer { - if let span = span { - self.finishTrackingNSData(data ?? Data(), span: span) + if let data = data { + span.setData(value: data.count, key: SentrySpanKey.fileSize) } + span.finish() } return method(path, data, attr) } @@ -101,15 +106,17 @@ extension SentryFileIOTracker { origin: String, method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void ) throws { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. guard srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { return try method(srcUrl, dstUrl) } - let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileCopy, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileCopy) else { + return try method(srcUrl, dstUrl) } defer { - span?.finish() + span.finish() } try method(srcUrl, dstUrl) } @@ -120,12 +127,11 @@ extension SentryFileIOTracker { origin: String, method: (_ srcPath: String, _ dstPath: String) throws -> Void ) throws { - let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileCopy, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileCopy) else { + return try method(srcPath, dstPath) } defer { - span?.finish() + span.finish() } try method(srcPath, dstPath) } @@ -136,15 +142,17 @@ extension SentryFileIOTracker { origin: String, method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void ) throws { + // We dont track reads from a url that is not a file url + // because these reads are handled by NSURLSession and + // SentryNetworkTracker will create spans in these cases. guard srcUrl.scheme == NSURLFileScheme && dstUrl.scheme == NSURLFileScheme else { return try method(srcUrl, dstUrl) } - let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileRename, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: srcUrl.path, origin: origin, operation: SentrySpanOperation.fileRename) else { + return try method(srcUrl, dstUrl) } defer { - span?.finish() + span.finish() } try method(srcUrl, dstUrl) } @@ -155,12 +163,11 @@ extension SentryFileIOTracker { origin: String, method: (_ srcPath: String, _ dstPath: String) throws -> Void ) throws { - let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileRename, size: 0) - defer { - self.endTrackingFile() + guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileRename) else { + return try method(srcPath, dstPath) } defer { - span?.finish() + span.finish() } try method(srcPath, dstPath) } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index f2a7dfeb2d2..78015c118ca 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -1,9 +1,9 @@ @objcMembers public class SentryExperimentalOptions: NSObject { /** - * Enables swizzling of`NSData` to automatically track file operations. + * Disables swizzling of`NSData` to automatically track file operations. */ - public var enableDataSwizzling = false + public var disableDataSwizzling = false /** * Enables swizzling of`NSFileManager` to automatically track file operations. diff --git a/Sources/Swift/Transactions/SentrySpanKey.swift b/Sources/Swift/Transactions/SentrySpanKey.swift new file mode 100644 index 00000000000..38297a4f048 --- /dev/null +++ b/Sources/Swift/Transactions/SentrySpanKey.swift @@ -0,0 +1,7 @@ +import Foundation + +@objcMembers @objc(SentrySpanKey) +class SentrySpanKey: NSObject { + /// Used to set the number of bytes processed in a file span operation + static let fileSize = "file.size" +} diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index 08ddd24be22..c32cdbeebf6 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -64,10 +64,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToRead.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) } func testInitContentsOfUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { @@ -80,10 +80,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileRead) - XCTAssertEqual(span.data["file.size"] as? Int, 0) XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToRead.path) + XCTAssertNil(span.data["file.size"]) } // MARK: - Data.writeWithSentryTracing(to:) @@ -99,10 +99,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { // -- Assert -- XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToWrite.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) // Reading the written data will create a span, so do it after asserting the transaction let writtenData = try Data(contentsOf: fixture.fileUrlToWrite) @@ -119,9 +119,9 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) - XCTAssertEqual(span.origin, SentryTraceOrigin.autoNSData) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) } } diff --git a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift new file mode 100644 index 00000000000..b494adc04ce --- /dev/null +++ b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift @@ -0,0 +1,354 @@ +@testable import Sentry +import SentryTestUtils +import XCTest + +class FileManagerSentryTracingIntegrationTests: XCTestCase { + private class Fixture { + + let data = "SOME DATA".data(using: .utf8)! + + var fileSrcUrl: URL! + var fileDestUrl: URL! + + init() {} + + func getSut(testName: String) throws -> FileManager { + let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("test-\(testName.hashValue.description)") + try! FileManager.default + .createDirectory(at: tempDir, withIntermediateDirectories: true) + + fileSrcUrl = tempDir.appendingPathComponent("source-file") + try data.write(to: fileSrcUrl) + + fileDestUrl = tempDir.appendingPathComponent("destination-file") + + // Initialize the SDK after files are written, so preparations are not traced + SentrySDK.start { options in + options.enableSwizzling = true + options.enableAutoPerformanceTracing = true + options.enableFileIOTracing = true + options.tracesSampleRate = 1.0 + options.setIntegrations([SentryFileIOTrackingIntegration.self]) + } + + return FileManager.default + } + + var fileSrcPath: String { fileSrcUrl.path } + var invalidSrcUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } + var invalidSrcPath: String { invalidSrcUrl.path } + + var fileDestPath: String { fileDestUrl.path } + var invalidDestUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } + var invalidDestPath: String { invalidDestUrl.path } + + var fileUrlToDelete: URL { fileSrcUrl } + var filePathToDelete: String { fileUrlToDelete.path } + var invalidUrlToDelete: URL { invalidSrcUrl } + var invalidPathToDelete: String { invalidSrcPath } + + var fileUrlToCreate: URL { fileDestUrl } + var filePathToCreate: String { fileUrlToCreate.path } + var invalidUrlToCreate: URL { invalidDestUrl } + var invalidPathToCreate: String { invalidDestPath } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() + } + + // MARK: - FileManager.createFileWithSentryTracing(atPath:contents:attributes:) + + func testCreateFileWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.filePathToCreate) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + + // Reading the written data will create a span, so do it after asserting the transaction + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, fixture.data) + } + + func testCreateFileWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.invalidPathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertFalse(result) + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidPathToCreate) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + } + + // MARK: - FileManager.removeItemWithSentryTracing(at:) + + func testRemoveItemWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.fileUrlToDelete) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToDelete.path) + } + + func testRemoveItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(at: fixture.invalidUrlToDelete)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidPathToDelete) + } + + // MARK: - FileManager.removeItemWithSentryTracing(atPath:) + + func testRemoveItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + let isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileCreated) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.filePathToDelete) + + // -- Assert -- + let isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testRemoveItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(atPath: fixture.invalidSrcPath)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + // MARK: - FileManager.copyItemWithSentryTracing(at:to:) + + func testCopyItemWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) + } + + func testCopyItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.invalidSrcUrl, to: fixture.invalidDestUrl)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + // MARK: - FileManager.copyItemWithSentryTracing(atPath:toPath:) + + func testCopyItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + // Smoke test to ensure the file is written + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testCopyItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.invalidSrcPath, to: fixture.invalidDestPath)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } + + // MARK: - FileManager.moveItemWithSentryTracing(at:to:) + + func testMoveItemWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) + } + + func testMoveItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.invalidSrcUrl, to: fixture.invalidDestUrl)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcUrl.path) + } + + // MARK: - FileManager.moveItemWithSentryTracing(at:to:) + + func testMoveItemAtPathWithSentryTracing_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Smoke test to ensure the file is written + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) + } + + func testMoveItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act & Assert -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.invalidSrcPath, to: fixture.invalidDestPath)) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + } +} diff --git a/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift b/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift new file mode 100644 index 00000000000..efe1cd38303 --- /dev/null +++ b/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift @@ -0,0 +1,8 @@ +@testable import Sentry +import XCTest + +class SentrySpanKeyTests: XCTestCase { + func testAppLifecycle_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanKey.fileSize, "file.size") + } +} diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index 87c0f53ffeb..d370346ab01 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -19.1.6 +19.1.7 diff --git a/scripts/.swiftlint-version b/scripts/.swiftlint-version index d139b327db8..31cbc49ac73 100644 --- a/scripts/.swiftlint-version +++ b/scripts/.swiftlint-version @@ -1 +1 @@ -0.57.1 +0.58.2 From 732f3940479300d28d86e1c84fc9b13cd243af1c Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 11:43:20 +0100 Subject: [PATCH 33/38] wip --- .../xcshareddata/xcschemes/SentryTests.xcscheme | 3 +-- Sources/Sentry/SentryNSDataSwizzling.m | 2 +- Sources/Swift/SentryExperimentalOptions.swift | 6 ++++++ .../Performance/IO/DataSentryTracingIntegrationTests.swift | 2 ++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme b/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme index 26fdfd4936c..52b04c2e68b 100644 --- a/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme +++ b/Sentry.xcodeproj/xcshareddata/xcschemes/SentryTests.xcscheme @@ -10,8 +10,7 @@ buildConfiguration = "Test" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 191a8383a28..e62e2318f37 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -24,7 +24,7 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * self.tracker = tracker; if (!options.enableSwizzling || options.experimental.disableDataSwizzling) { - SENTRY_LOG_DEBUG(@"Experimental auto-tracking of NSData is disabled") + SENTRY_LOG_DEBUG(@"Auto-tracking of NSData is disabled") return; } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index 78015c118ca..0a4703ade5a 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -2,11 +2,17 @@ public class SentryExperimentalOptions: NSObject { /** * Disables swizzling of`NSData` to automatically track file operations. + * + * - Note: Swizzling is enabled by setting ``SentryOptions.enableSwizzling`` to `true`. + * This option allows you to disable swizzling for `NSData` only, while keeping swizzling enabled for other classes. + * This is useful if you want to use manual tracing for file operations. */ public var disableDataSwizzling = false /** * Enables swizzling of`NSFileManager` to automatically track file operations. + * + * - Requires: Swizzling must be enabled by setting ``SentryOptions.enableSwizzling`` to `true`. */ public var enableFileManagerSwizzling = false diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index c32cdbeebf6..f6b1196eb0a 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -26,6 +26,8 @@ class DataSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in options.enableSwizzling = true + // NOTE: We are disabling data swizzling to test the integration on older systems where swizzling of NSData was still supported + options.experimental.disableDataSwizzling = true options.enableAutoPerformanceTracing = true options.enableFileIOTracing = true options.tracesSampleRate = 1.0 From 210509736ca70f9d4b47d4fea927641a11756826 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 12:24:04 +0100 Subject: [PATCH 34/38] disable swizzling in file io tests --- .../Performance/IO/DataSentryTracingIntegrationTests.swift | 7 ++++--- .../IO/FileManagerTracingIntegrationTests.swift | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index f6b1196eb0a..0998b39a05f 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -25,9 +25,10 @@ class DataSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in - options.enableSwizzling = true - // NOTE: We are disabling data swizzling to test the integration on older systems where swizzling of NSData was still supported - options.experimental.disableDataSwizzling = true + // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. + // Instead we are recommending to disable swizzling and use manual tracing. + options.enableSwizzling = false + options.enableAutoPerformanceTracing = true options.enableFileIOTracing = true options.tracesSampleRate = 1.0 diff --git a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift index b494adc04ce..1158ebbd3cf 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift @@ -25,7 +25,10 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in - options.enableSwizzling = true + // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. + // Instead we are recommending to disable swizzling and use manual tracing. + options.enableSwizzling = false + options.enableAutoPerformanceTracing = true options.enableFileIOTracing = true options.tracesSampleRate = 1.0 From c08b59888c022c9428e5f23309e6ff7fdb0b3608 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 12:26:35 +0100 Subject: [PATCH 35/38] fix test configuration --- .../IO/DataSentryTracingIntegrationTests.swift | 13 ++++++++----- .../IO/FileManagerTracingIntegrationTests.swift | 17 +++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index 0998b39a05f..3d30b1094db 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -25,14 +25,17 @@ class DataSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in - // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. - // Instead we are recommending to disable swizzling and use manual tracing. - options.enableSwizzling = false - + // Configure options required by File I/O tracking integration options.enableAutoPerformanceTracing = true options.enableFileIOTracing = true - options.tracesSampleRate = 1.0 options.setIntegrations([SentryFileIOTrackingIntegration.self]) + + // Configure the tracing sample rate to record all traces + options.tracesSampleRate = 1.0 + + // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. + // Instead we are recommending to disable swizzling and use manual tracing. + options.enableSwizzling = false } } diff --git a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift index 1158ebbd3cf..823bf8e77d8 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift @@ -25,14 +25,17 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in - // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. - // Instead we are recommending to disable swizzling and use manual tracing. - options.enableSwizzling = false - + // Configure options required by File I/O tracking integration options.enableAutoPerformanceTracing = true options.enableFileIOTracing = true - options.tracesSampleRate = 1.0 options.setIntegrations([SentryFileIOTrackingIntegration.self]) + + // Configure the tracing sample rate to record all traces + options.tracesSampleRate = 1.0 + + // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. + // Instead we are recommending to disable swizzling and use manual tracing. + options.enableSwizzling = false } return FileManager.default @@ -51,9 +54,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { var invalidUrlToDelete: URL { invalidSrcUrl } var invalidPathToDelete: String { invalidSrcPath } - var fileUrlToCreate: URL { fileDestUrl } - var filePathToCreate: String { fileUrlToCreate.path } - var invalidUrlToCreate: URL { invalidDestUrl } + var filePathToCreate: String { fileDestUrl.path } var invalidPathToCreate: String { invalidDestPath } } From 8b609d93c1c8395ee58704dd216e40873d5f0448 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 12:32:43 +0100 Subject: [PATCH 36/38] add more tests --- Sources/Sentry/SentryFileIOTracker.m | 8 +++++--- Sources/Swift/Transactions/SentrySpanKey.swift | 3 +++ Tests/SentryTests/Transactions/SentrySpanKeyTests.swift | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/SentryFileIOTracker.m b/Sources/Sentry/SentryFileIOTracker.m index 5e82a256b01..450176a6287 100644 --- a/Sources/Sentry/SentryFileIOTracker.m +++ b/Sources/Sentry/SentryFileIOTracker.m @@ -231,7 +231,8 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path fileSize:size]]; ioSpan.origin = origin; if (size > 0) { - [ioSpan setDataValue:[NSNumber numberWithUnsignedInteger:size] forKey:@"file.size"]; + [ioSpan setDataValue:[NSNumber numberWithUnsignedInteger:size] + forKey:SentrySpanKey.fileSize]; } }]; @@ -243,7 +244,7 @@ - (BOOL)measureNSFileManagerCreateFileAtPath:(NSString *)path SENTRY_LOG_DEBUG(@"Automatically started a new span with description: %@, operation: %@", ioSpan.description, operation); - [ioSpan setDataValue:path forKey:@"file.path"]; + [ioSpan setDataValue:path forKey:SentrySpanKey.filePath]; [self mainThreadExtraInfo:ioSpan]; @@ -325,7 +326,8 @@ - (void)endTrackingFile - (void)finishTrackingNSData:(NSData *)data span:(id)span { - [span setDataValue:[NSNumber numberWithUnsignedInteger:data.length] forKey:@"file.size"]; + [span setDataValue:[NSNumber numberWithUnsignedInteger:data.length] + forKey:SentrySpanKey.fileSize]; [span finish]; SENTRY_LOG_DEBUG(@"Automatically finished span %@", span.description); diff --git a/Sources/Swift/Transactions/SentrySpanKey.swift b/Sources/Swift/Transactions/SentrySpanKey.swift index 38297a4f048..7535d0d5d2c 100644 --- a/Sources/Swift/Transactions/SentrySpanKey.swift +++ b/Sources/Swift/Transactions/SentrySpanKey.swift @@ -4,4 +4,7 @@ import Foundation class SentrySpanKey: NSObject { /// Used to set the number of bytes processed in a file span operation static let fileSize = "file.size" + + /// Used to set the path of the file in a file span operation + static let filePath = "file.path" } diff --git a/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift b/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift index efe1cd38303..4ab1f9487a0 100644 --- a/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift +++ b/Tests/SentryTests/Transactions/SentrySpanKeyTests.swift @@ -2,7 +2,11 @@ import XCTest class SentrySpanKeyTests: XCTestCase { - func testAppLifecycle_shouldBeExpectedValue() { + func testFileSize_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanKey.fileSize, "file.size") } + + func testFilePath_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanKey.filePath, "file.path") + } } From b54b4f3fad138008c9a4b6365fcdf7ca4e44da9b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 12:38:43 +0100 Subject: [PATCH 37/38] Add more tests --- .../Transactions/SentrySpanOperationTests.swift | 12 ++++++++++++ .../Transactions/SentryTraceOriginTests.swift | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift index f6e45bb3c49..28321751700 100644 --- a/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift +++ b/Tests/SentryTests/Transactions/SentrySpanOperationTests.swift @@ -22,6 +22,18 @@ class SentrySpanOperationTests: XCTestCase { XCTAssertEqual(SentrySpanOperation.fileWrite, "file.write") } + func testFileCopy_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanOperation.fileCopy, "file.copy") + } + + func testFileRename_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanOperation.fileRename, "file.rename") + } + + func testFileDelete_shouldBeExpectedValue() { + XCTAssertEqual(SentrySpanOperation.fileDelete, "file.delete") + } + func testNetworkRequestOperation_shouldBeExpectedValue() { XCTAssertEqual(SentrySpanOperation.networkRequestOperation, "http.client") } diff --git a/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift b/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift index 1d50c020344..57702751bf0 100644 --- a/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift +++ b/Tests/SentryTests/Transactions/SentryTraceOriginTests.swift @@ -38,6 +38,10 @@ class SentryTraceOriginTestsTests: XCTestCase { XCTAssertEqual(SentryTraceOrigin.manual, "manual") } + func testManualFileData_shouldBeExpectedValue() { + XCTAssertEqual(SentryTraceOrigin.manualFileData, "manual.file.data") + } + func testManualUITimeToDisplay_shouldBeExpectedValue() { XCTAssertEqual(SentryTraceOrigin.manualUITimeToDisplay, "manual.ui.time_to_display") } From eb8ed5d21720e8f299c2e01f6500e43314e77820 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 13 Feb 2025 16:08:17 +0100 Subject: [PATCH 38/38] add tests --- .../IO/SentryFileIOTracker+SwiftHelpers.swift | 16 +- .../DataSentryTracingIntegrationTests.swift | 117 +++- .../FileManagerTracingIntegrationTests.swift | 551 +++++++++++++++++- 3 files changed, 642 insertions(+), 42 deletions(-) diff --git a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift index 729416d2cf4..80cf4945ce0 100644 --- a/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift +++ b/Sources/Swift/Integrations/Performance/IO/SentryFileIOTracker+SwiftHelpers.swift @@ -6,7 +6,7 @@ extension SentryFileIOTracker { options: Data.ReadingOptions, origin: String, method: (_ url: URL, _ options: Data.ReadingOptions) throws -> Data - ) throws -> Data { + ) rethrows -> Data { // We dont track reads from a url that is not a file url // because these reads are handled by NSURLSession and // SentryNetworkTracker will create spans in these cases. @@ -30,7 +30,7 @@ extension SentryFileIOTracker { options: Data.WritingOptions, origin: String, method: (_ data: Data, _ url: URL, _ options: Data.WritingOptions) throws -> Void - ) throws { + ) rethrows { // We dont track reads from a url that is not a file url // because these reads are handled by NSURLSession and // SentryNetworkTracker will create spans in these cases. @@ -50,7 +50,7 @@ extension SentryFileIOTracker { at url: URL, origin: String, method: (_ url: URL) throws -> Void - ) throws { + ) rethrows { // We dont track reads from a url that is not a file url // because these reads are handled by NSURLSession and // SentryNetworkTracker will create spans in these cases. @@ -70,7 +70,7 @@ extension SentryFileIOTracker { atPath path: String, origin: String, method: (_ path: String) throws -> Void - ) throws { + ) rethrows { guard let span = self.span(forPath: path, origin: origin, operation: SentrySpanOperation.fileDelete) else { return try method(path) } @@ -105,7 +105,7 @@ extension SentryFileIOTracker { to dstUrl: URL, origin: String, method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void - ) throws { + ) rethrows { // We dont track reads from a url that is not a file url // because these reads are handled by NSURLSession and // SentryNetworkTracker will create spans in these cases. @@ -126,7 +126,7 @@ extension SentryFileIOTracker { toPath dstPath: String, origin: String, method: (_ srcPath: String, _ dstPath: String) throws -> Void - ) throws { + ) rethrows { guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileCopy) else { return try method(srcPath, dstPath) } @@ -141,7 +141,7 @@ extension SentryFileIOTracker { to dstUrl: URL, origin: String, method: (_ srcUrl: URL, _ dstUrl: URL) throws -> Void - ) throws { + ) rethrows { // We dont track reads from a url that is not a file url // because these reads are handled by NSURLSession and // SentryNetworkTracker will create spans in these cases. @@ -162,7 +162,7 @@ extension SentryFileIOTracker { toPath dstPath: String, origin: String, method: (_ srcPath: String, _ dstPath: String) throws -> Void - ) throws { + ) rethrows { guard let span = self.span(forPath: srcPath, origin: origin, operation: SentrySpanOperation.fileRename) else { return try method(srcPath, dstPath) } diff --git a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift index 3d30b1094db..a0b1c5ff2eb 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/DataSentryTracingIntegrationTests.swift @@ -9,10 +9,11 @@ class DataSentryTracingIntegrationTests: XCTestCase { var fileUrlToRead: URL! var fileUrlToWrite: URL! + var ignoredFileUrl: URL! init() {} - func getSut(testName: String) throws { + func getSut(testName: String, isEnabled: Bool = true) throws -> Data { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("test-\(testName.hashValue.description)") try! FileManager.default @@ -25,10 +26,12 @@ class DataSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in + options.removeAllIntegrations() + // Configure options required by File I/O tracking integration options.enableAutoPerformanceTracing = true - options.enableFileIOTracing = true - options.setIntegrations([SentryFileIOTrackingIntegration.self]) + options.enableFileIOTracing = isEnabled + options.setIntegrations(isEnabled ? [SentryFileIOTrackingIntegration.self] : []) // Configure the tracing sample rate to record all traces options.tracesSampleRate = 1.0 @@ -36,7 +39,19 @@ class DataSentryTracingIntegrationTests: XCTestCase { // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. // Instead we are recommending to disable swizzling and use manual tracing. options.enableSwizzling = false + + // Configure the cache directory to a temporary directory, so we can isolate the test files + options.cacheDirectoryPath = tempDir.path } + + // Get the working directory of the SDK, as these files are ignored by default + guard let sentryPath = SentrySDK.currentHub().getClient()?.fileManager.sentryPath else { + preconditionFailure("Sentry path is nil, but should be configured for test cases.") + } + ignoredFileUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-file") + try data.write(to: ignoredFileUrl) + + return data } var invalidFileUrlToRead: URL { @@ -46,6 +61,11 @@ class DataSentryTracingIntegrationTests: XCTestCase { var invalidFileUrlToWrite: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } + + var nonFileUrl: URL { + // URL to a file that is not a file but should exist at all times + URL(string: "https://raw.githubusercontent.com/getsentry/sentry-cocoa/refs/heads/main/.gitignore")! + } } private var fixture: Fixture! @@ -55,6 +75,11 @@ class DataSentryTracingIntegrationTests: XCTestCase { fixture = Fixture() } + override func tearDown() { + super.tearDown() + clearTestState() + } + // MARK: - Data.init(contentsOfUrlWithSentryTracing:) func testInitContentsOfUrlWithSentryTracing_shouldTraceManually() throws { @@ -92,15 +117,54 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertNil(span.data["file.size"]) } + func testInitContentsOfUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfUrlWithSentryTracing: fixture.nonFileUrl) + + // -- Assert -- + XCTAssertGreaterThan(data.count, 0) + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testInitContentsOfUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfUrlWithSentryTracing: fixture.fileUrlToRead) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testInitContentsOfUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let _ = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + let data = try Data(contentsOfUrlWithSentryTracing: fixture.ignoredFileUrl) + + // -- Assert -- + XCTAssertEqual(data, fixture.data) + XCTAssertEqual(parentTransaction.children.count, 0) + } + // MARK: - Data.writeWithSentryTracing(to:) func testWriteWithSentryTracing_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- - let _ = try fixture.getSut(testName: self.name) + let sut: Data = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) // -- Act -- - try fixture.data.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: .atomic) + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: .atomic) // -- Assert -- XCTAssertEqual(parentTransaction.children.count, 1) @@ -117,11 +181,11 @@ class DataSentryTracingIntegrationTests: XCTestCase { func testWriteWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- - let _ = try fixture.getSut(testName: self.name) + let sut: Data = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) // -- Act & Assert -- - XCTAssertThrowsError(try fixture.data.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) + XCTAssertThrowsError(try sut.writeWithSentryTracing(to: fixture.invalidFileUrlToWrite)) XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) @@ -130,4 +194,43 @@ class DataSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidFileUrlToWrite.path) XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) } + + func testWriteWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.writeWithSentryTracing(to: fixture.nonFileUrl, options: .atomic)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testWriteWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.fileUrlToWrite, options: .atomic) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testWriteWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut: Data = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + try sut.writeWithSentryTracing(to: fixture.ignoredFileUrl, options: .atomic) + + // -- Assert -- + let writtenData = try Data(contentsOf: fixture.ignoredFileUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } } diff --git a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift index 823bf8e77d8..03868021fd9 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/FileManagerTracingIntegrationTests.swift @@ -9,10 +9,13 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { var fileSrcUrl: URL! var fileDestUrl: URL! + var ignoredFileToDeleteUrl: URL! + var ignoredFileToCreateUrl: URL! + var ignoredSrcFileUrl: URL! init() {} - func getSut(testName: String) throws -> FileManager { + func getSut(testName: String, isEnabled: Bool = true) throws -> FileManager { let tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("test-\(testName.hashValue.description)") try! FileManager.default @@ -25,10 +28,12 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { // Initialize the SDK after files are written, so preparations are not traced SentrySDK.start { options in + options.removeAllIntegrations() + // Configure options required by File I/O tracking integration options.enableAutoPerformanceTracing = true - options.enableFileIOTracing = true - options.setIntegrations([SentryFileIOTrackingIntegration.self]) + options.enableFileIOTracing = isEnabled + options.setIntegrations(isEnabled ? [SentryFileIOTrackingIntegration.self] : []) // Configure the tracing sample rate to record all traces options.tracesSampleRate = 1.0 @@ -36,14 +41,28 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { // NOTE: We are not testing for the case where swizzling is enabled, as it could lead to duplicate spans on older OS versions. // Instead we are recommending to disable swizzling and use manual tracing. options.enableSwizzling = false + + // Configure the cache directory to a temporary directory, so we can isolate the test files + options.cacheDirectoryPath = tempDir.path } + // Get the working directory of the SDK, as these files are ignored by default + let sentryPath = SentrySDK.currentHub().getClient()!.fileManager.sentryPath + ignoredFileToCreateUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-file-to-create") + + ignoredFileToDeleteUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-file-to-delete") + try data.write(to: ignoredFileToDeleteUrl) + + ignoredSrcFileUrl = URL(fileURLWithPath: sentryPath).appendingPathComponent("ignored-src-file") + try data.write(to: ignoredSrcFileUrl) + return FileManager.default } var fileSrcPath: String { fileSrcUrl.path } var invalidSrcUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } var invalidSrcPath: String { invalidSrcUrl.path } + var ignoredSrcFilePath: String { ignoredSrcFileUrl.path } var fileDestPath: String { fileDestUrl.path } var invalidDestUrl: URL { URL(fileURLWithPath: "/path/that/does/not/exist") } @@ -56,6 +75,19 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { var filePathToCreate: String { fileDestUrl.path } var invalidPathToCreate: String { invalidDestPath } + + var nonFileUrl: URL { + // URL to a file that is not a file but should exist at all times + URL(string: "https://raw.githubusercontent.com/getsentry/sentry-cocoa/refs/heads/main/.gitignore")! + } + + var ignoredFileToCreatePath: String { + ignoredFileToCreateUrl.path + } + + var ignoredFileToDeletePath: String { + ignoredFileToDeleteUrl.path + } } private var fixture: Fixture! @@ -65,40 +97,88 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { fixture = Fixture() } + override func tearDown() { + super.tearDown() + clearTestState() + } + // MARK: - FileManager.createFileWithSentryTracing(atPath:contents:attributes:) - func testCreateFileWithSentryTracing_shouldTraceManually() throws { + func testCreateFileAtPathWithSentryTracing_withoutData_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + // -- Act -- - let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: nil) // -- Assert -- XCTAssertTrue(result) + + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) + + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData.count, 0) + XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) XCTAssertEqual(span.data["file.path"] as? String, fixture.filePathToCreate) - XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) + XCTAssertNil(span.data["file.size"]) + } + + func testCreateFileAtPathWithSentryTracing_withData_shouldTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) - // Reading the written data will create a span, so do it after asserting the transaction let writtenData = try Data(contentsOf: fixture.fileDestUrl) XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 1) + let span = try XCTUnwrap(parentTransaction.children.first) + XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) + XCTAssertEqual(span.operation, SentrySpanOperation.fileWrite) + XCTAssertEqual(span.data["file.path"] as? String, fixture.filePathToCreate) + XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) } - func testCreateFileWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + func testCreateFileAtPathWithSentryTracing_failsToCreateFile_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - + + // Check pre-condition + var isFileCreated = FileManager.default.fileExists(atPath: fixture.invalidPathToCreate) + XCTAssertFalse(isFileCreated) + // -- Act -- let result = sut.createFileWithSentryTracing(atPath: fixture.invalidPathToCreate, contents: fixture.data) // -- Assert -- XCTAssertFalse(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) @@ -107,17 +187,66 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.size"] as? Int, fixture.data.count) } + func testCreateFileAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.filePathToCreate, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToCreate) + XCTAssertTrue(isFileCreated) + let writtenData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCreateFileAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileCreated = FileManager.default.fileExists(atPath: fixture.ignoredFileToCreatePath) + XCTAssertFalse(isFileCreated) + + // -- Act -- + let result = sut.createFileWithSentryTracing(atPath: fixture.ignoredFileToCreatePath, contents: fixture.data) + + // -- Assert -- + XCTAssertTrue(result) + isFileCreated = FileManager.default.fileExists(atPath: fixture.ignoredFileToCreatePath) + XCTAssertTrue(isFileCreated) + let writtenData = try Data(contentsOf: fixture.ignoredFileToCreateUrl) + XCTAssertEqual(writtenData, fixture.data) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + // MARK: - FileManager.removeItemWithSentryTracing(at:) - func testRemoveItemWithSentryTracing_shouldTraceManually() throws { + func testRemoveItemAtUrlWithSentryTracing_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + // Check pre-condition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + // -- Act -- try sut.removeItemWithSentryTracing(at: fixture.fileUrlToDelete) // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) @@ -125,7 +254,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.fileUrlToDelete.path) } - func testRemoveItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + func testRemoveItemAtUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -140,6 +269,56 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidPathToDelete) } + func testRemoveItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(at: fixture.nonFileUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check-precondition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.fileUrlToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeleteUrl.path) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(at: fixture.ignoredFileToDeleteUrl) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeleteUrl.path) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + // MARK: - FileManager.removeItemWithSentryTracing(atPath:) func testRemoveItemAtPathWithSentryTracing_shouldTraceManually() throws { @@ -148,14 +327,14 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) // Smoke test to ensure the file is written - let isFileCreated = FileManager.default.fileExists(atPath: fixture.filePathToDelete) - XCTAssertTrue(isFileCreated) + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) // -- Act -- try sut.removeItemWithSentryTracing(atPath: fixture.filePathToDelete) // -- Assert -- - let isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) XCTAssertTrue(isFileRemoved) XCTAssertEqual(parentTransaction.children.count, 1) @@ -171,7 +350,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) // -- Act & Assert -- - XCTAssertThrowsError(try sut.removeItemWithSentryTracing(atPath: fixture.invalidSrcPath)) + XCTAssertThrowsError(try sut.removeItemWithSentryTracing(atPath: fixture.invalidPathToDelete)) XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) @@ -179,15 +358,57 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.operation, SentrySpanOperation.fileDelete) XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) } + + func testRemoveItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.filePathToDelete) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.filePathToDelete) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testRemoveItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-condition + var isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeletePath) + XCTAssertFalse(isFileRemoved) + + // -- Act -- + try sut.removeItemWithSentryTracing(atPath: fixture.ignoredFileToDeletePath) + + // -- Assert -- + isFileRemoved = !FileManager.default.fileExists(atPath: fixture.ignoredFileToDeletePath) + XCTAssertTrue(isFileRemoved) + + XCTAssertEqual(parentTransaction.children.count, 0) + } // MARK: - FileManager.copyItemWithSentryTracing(at:to:) - func testCopyItemWithSentryTracing_shouldTraceManually() throws { + func testCopyItemAtUrlWithSentryTracing_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - // Smoke test to ensure the file is written + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + let srcData = try Data(contentsOf: fixture.fileSrcUrl) XCTAssertEqual(srcData, fixture.data) @@ -195,6 +416,11 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { try sut.copyItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + let destData = try Data(contentsOf: fixture.fileDestUrl) XCTAssertEqual(destData, fixture.data) @@ -205,7 +431,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) } - func testCopyItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + func testCopyItemAtUrlWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -217,7 +443,77 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) XCTAssertEqual(span.operation, SentrySpanOperation.fileCopy) - XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) + XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcUrl.path) + } + + func testCopyItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.ignoredSrcFileUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.copyItemWithSentryTracing(at: fixture.nonFileUrl, to: fixture.fileDestUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) } // MARK: - FileManager.copyItemWithSentryTracing(atPath:toPath:) @@ -227,12 +523,15 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - // Smoke test to ensure the file is written + // Check pre-conditions var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) XCTAssertTrue(isSrcFileExisting) var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) XCTAssertFalse(isDestFileExisting) + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + // -- Act -- try sut.copyItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) @@ -244,7 +543,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertTrue(isDestFileExisting) let writtenData = try Data(contentsOf: fixture.fileDestUrl) - XCTAssertEqual(writtenData, fixture.data) + XCTAssertEqual(writtenData, srcData) XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) @@ -268,19 +567,80 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) } + func testCopyItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testCopyItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.copyItemWithSentryTracing(at: fixture.ignoredSrcFilePath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + // MARK: - FileManager.moveItemWithSentryTracing(at:to:) - func testMoveItemWithSentryTracing_shouldTraceManually() throws { + func testMoveItemAtUrlWithSentryTracing_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - // Smoke test to ensure the file is written + // Check pre-conditions var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) XCTAssertTrue(isSrcFileExisting) var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) XCTAssertFalse(isDestFileExisting) + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + // -- Act -- try sut.moveItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) @@ -290,6 +650,9 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) XCTAssertTrue(isDestFileExisting) + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) @@ -297,7 +660,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcUrl.path) } - func testMoveItemWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + func testMoveItemAtUrlWithSentryTracing_throwsError_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -312,19 +675,92 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcUrl.path) } - // MARK: - FileManager.moveItemWithSentryTracing(at:to:) + func testMoveItemAtUrlWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtUrlWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.ignoredSrcFileUrl, to: fixture.fileDestUrl) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFileUrl.path) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestUrl.path) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtUrlWithSentryTracing_nonFileUrl_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // -- Act -- + XCTAssertThrowsError(try sut.moveItemWithSentryTracing(at: fixture.nonFileUrl, to: fixture.fileDestUrl)) + + // -- Assert -- + XCTAssertEqual(parentTransaction.children.count, 0) + } + + // MARK: - FileManager.moveItemWithSentryTracing(atPath:toPath:) func testMoveItemAtPathWithSentryTracing_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) - // Smoke test to ensure the file is written + // Check pre-conditions var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) XCTAssertTrue(isSrcFileExisting) var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) XCTAssertFalse(isDestFileExisting) + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + // -- Act -- try sut.moveItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) @@ -334,6 +770,9 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) XCTAssertTrue(isDestFileExisting) + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + XCTAssertEqual(parentTransaction.children.count, 1) let span = try XCTUnwrap(parentTransaction.children.first) XCTAssertEqual(span.origin, SentryTraceOrigin.manualFileData) @@ -341,7 +780,7 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.data["file.path"] as? String, fixture.fileSrcPath) } - func testMoveItemAtPathWithSentryTracing_throwsError_shouldTraceManuallyWithErrorRethrow() throws { + func testMoveItemAtPathWithSentryTracing_throwsError_shouldTraceManually() throws { // -- Arrange -- let sut = try fixture.getSut(testName: self.name) let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) @@ -355,4 +794,62 @@ class FileManagerSentryTracingIntegrationTests: XCTestCase { XCTAssertEqual(span.operation, SentrySpanOperation.fileRename) XCTAssertEqual(span.data["file.path"] as? String, fixture.invalidSrcPath) } + + func testMoveItemAtPathWithSentryTracing_trackerIsNotEnabled_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name, isEnabled: false) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.fileSrcPath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.fileSrcPath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } + + func testMoveItemAtPathWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws { + // -- Arrange -- + let sut = try fixture.getSut(testName: self.name) + let parentTransaction = try XCTUnwrap(SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) as? SentryTracer) + + // Check pre-conditions + var isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertTrue(isSrcFileExisting) + var isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertFalse(isDestFileExisting) + + let srcData = try Data(contentsOf: fixture.fileSrcUrl) + XCTAssertEqual(srcData, fixture.data) + + // -- Act -- + try sut.moveItemWithSentryTracing(at: fixture.ignoredSrcFilePath, to: fixture.fileDestPath) + + // -- Assert -- + isSrcFileExisting = FileManager.default.fileExists(atPath: fixture.ignoredSrcFilePath) + XCTAssertFalse(isSrcFileExisting) + isDestFileExisting = FileManager.default.fileExists(atPath: fixture.fileDestPath) + XCTAssertTrue(isDestFileExisting) + + let destData = try Data(contentsOf: fixture.fileDestUrl) + XCTAssertEqual(destData, srcData) + + XCTAssertEqual(parentTransaction.children.count, 0) + } }