From f1f5a449456060d5b8b12213364f77f88ee0a488 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 6 Dec 2024 13:02:54 +0100 Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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 f44e0302d23cddc2dc4348835d35b319141df9fb Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 10:54:15 +0100 Subject: [PATCH 08/32] revert changes after splitting PR --- CHANGELOG.md | 2 +- Sentry.xcodeproj/project.pbxproj | 4 - Sources/Swift/Helper/SentryDataWrapper.swift | 602 ------------------ ...SentryFileIOTrackingIntegrationTests.swift | 113 +--- 4 files changed, 21 insertions(+), 700 deletions(-) delete mode 100644 Sources/Swift/Helper/SentryDataWrapper.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index c957ebb81d5..6bdd1e7413f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,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) +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18(#4546) ### Improvements diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index d2a3e9a795b..cd08f08613b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -782,7 +782,6 @@ 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 */; }; @@ -1860,7 +1859,6 @@ 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 = ""; }; @@ -2116,7 +2114,6 @@ 621D9F2D2B9B030E003D94DE /* Helper */ = { isa = PBXGroup; children = ( - D4EDF9832D0B2A1D0071E7B3 /* SentryDataWrapper.swift */, 84B0E0062CD963F9007FB332 /* SentryIconography.swift */, D8739CF62BECFF86007D2F66 /* Log */, 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */, @@ -4765,7 +4762,6 @@ 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 */, diff --git a/Sources/Swift/Helper/SentryDataWrapper.swift b/Sources/Swift/Helper/SentryDataWrapper.swift deleted file mode 100644 index 6289c9a06d1..00000000000 --- a/Sources/Swift/Helper/SentryDataWrapper.swift +++ /dev/null @@ -1,602 +0,0 @@ -// -// 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``. -@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 - 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(iOS 18, macOS 15, tvOS 18, *) -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(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. - 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/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index 44525a562f5..e1809633036 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -77,62 +77,26 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { assertWriteWithNoSpans() } - func testData_Writing_Tracking() { - 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 - // 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.write") { - try? fixture.data.write(to: fixture.fileURL) - } - } - - func testData_WritingWithOption_Tracking() { - 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 - // 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.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, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + func test_Writing_Tracking() throws { + if #available(iOS 18, macOS 15, tvOS 15, *) { + throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { - try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL) + try? 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 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + + func test_WritingWithOption_Tracking() throws { + if #available(iOS 18, macOS 15, tvOS 15, *) { + throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.write") { - try? SentryDataWrapper(data: fixture.data).write(to: fixture.fileURL, options: .atomic) + try? fixture.data.write(to: fixture.fileURL, options: .atomic) } } - + func test_ReadingTrackingDisabled_forIOOption() { SentrySDK.start(options: fixture.getOptions(enableFileIOTracing: false)) @@ -157,64 +121,27 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { assertWriteWithNoSpans() } - func testData_ReadingURL_Tracking() { - 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 - // 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 _ = try? Data(contentsOf: fixture.fileURL) - } - } - - func testData_ReadingURLWithOption_Tracking() throws { - 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 - // 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 18. - // Therefore, the wrapper 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") + func test_ReadingURL_Tracking() throws { + if #available(iOS 18, macOS 15, tvOS 15, *) { + throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { - let _ = try? SentryDataWrapper(contentsOf: fixture.fileURL) + let _ = try? Data(contentsOf: fixture.fileURL) } } - - func testDataWrapper_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. - guard #available(iOS 18, macOS 15, tvOS 18, *) else { - throw XCTSkip("SentryDataWrapper is not tested on this OS version") + + func test_ReadingURLWithOption_Tracking() throws { + if #available(iOS 18, macOS 15, tvOS 15, *) { + throw XCTSkip("File IO tracking for Swift.Data is disabled for this OS version") } SentrySDK.start(options: fixture.getOptions()) assertSpans(1, "file.read") { - let data = try? SentryDataWrapper(contentsOf: fixture.fileURL, options: .uncached) + 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") { From c3213ade4b0a0408b975c4554b385c17611c11f4 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 11:01:40 +0100 Subject: [PATCH 09/32] change PR number in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bdd1e7413f..439fa4d1a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ### Fixes - Fix GraphQL context for HTTP client error tracking (#4567) -- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18(#4546) +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18 (#4634) ### Improvements From 9c4aa63c1ffd8f340f6991dde02666f6bbcc1188 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 11:04:33 +0100 Subject: [PATCH 10/32] fix position of chaneglog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439fa4d1a1a..7dda0a4ac1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) +### Fixes + +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18 (#4634) + ## 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, macOS 15 and tvOS 18 (#4634) ### Improvements From 1ac59adf41cfd5ac81920d13dad8f85edf7e72d9 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 13 Dec 2024 13:23:46 +0100 Subject: [PATCH 11/32] feat: add experimental flag to enable filemanager swizzling --- Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m | 3 +++ Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 1 + Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift | 1 + Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift | 1 + Samples/macOS-Swift/macOS-Swift/AppDelegate.swift | 2 ++ Samples/macOS-SwiftUI/macOS-SwiftUI/macOS_SwiftUIApp.swift | 1 + Samples/tvOS-Swift/tvOS-SBSwift/AppDelegate.swift | 2 ++ Samples/tvOS-Swift/tvOS-Swift/AppDelegate.swift | 1 + Samples/visionOS-Swift/visionOS-Swift/VisionOSSwiftApp.swift | 1 + .../watchOS-Swift WatchKit Extension/ExtensionDelegate.swift | 1 + Sources/Sentry/SentryNSFileManagerSwizzling.m | 5 +++++ Sources/Swift/SentryExperimentalOptions.swift | 5 +++++ 12 files changed, 24 insertions(+) diff --git a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m index add7e76bfb4..c3e8eb95817 100644 --- a/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m +++ b/Samples/iOS-ObjectiveC/iOS-ObjectiveC/AppDelegate.m @@ -34,6 +34,9 @@ - (BOOL)application:(UIApplication *)application options.experimental.sessionReplay.sessionSampleRate = 0; options.experimental.sessionReplay.onErrorSampleRate = 1; + options.experimental.enableFileManagerSwizzling + = ![args containsObject:@"--disable-filemanager-swizzling"]; + options.initialScope = ^(SentryScope *scope) { [scope setTagValue:@"" forKey:@""]; [scope injectGitInformation]; diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 523bdd81d85..0b09c842e6f 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -113,6 +113,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.enableCoreDataTracing = !args.contains("--disable-core-data-tracing") options.enableNetworkBreadcrumbs = !args.contains("--disable-network-breadcrumbs") options.enableSwizzling = !args.contains("--disable-swizzling") + options.experimental.enableFileManagerSwizzling = !args.contains("--disable-filemanager-swizzling") options.enableCrashHandler = !args.contains("--disable-crash-handler") options.enableTracing = !args.contains("--disable-tracing") options.enablePersistingTracesWhenCrashing = true diff --git a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift index ee92d4a10c3..4084e9ebe67 100644 --- a/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift +++ b/Samples/iOS-SwiftUI/iOS-SwiftUI/SwiftUIApp.swift @@ -11,6 +11,7 @@ struct SwiftUIApp: App { options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 options.experimental.sessionReplay.sessionSampleRate = 1.0 + options.experimental.enableFileManagerSwizzling = true options.initialScope = { scope in scope.injectGitInformation() return scope diff --git a/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift b/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift index fa665811e5f..190ece526c7 100644 --- a/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift +++ b/Samples/iOS15-SwiftUI/iOS15-SwiftUI/App.swift @@ -9,6 +9,7 @@ struct SwiftUIApp: App { options.debug = true options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 + options.experimental.enableFileManagerSwizzling = true } } diff --git a/Samples/macOS-Swift/macOS-Swift/AppDelegate.swift b/Samples/macOS-Swift/macOS-Swift/AppDelegate.swift index cb0965a0655..d52d36b2897 100644 --- a/Samples/macOS-Swift/macOS-Swift/AppDelegate.swift +++ b/Samples/macOS-Swift/macOS-Swift/AppDelegate.swift @@ -35,6 +35,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { return scope } + + options.experimental.enableFileManagerSwizzling = !args.contains("--disable-filemanager-swizzling") } } diff --git a/Samples/macOS-SwiftUI/macOS-SwiftUI/macOS_SwiftUIApp.swift b/Samples/macOS-SwiftUI/macOS-SwiftUI/macOS_SwiftUIApp.swift index 3b1770fca27..2bab7d24af5 100644 --- a/Samples/macOS-SwiftUI/macOS-SwiftUI/macOS_SwiftUIApp.swift +++ b/Samples/macOS-SwiftUI/macOS-SwiftUI/macOS_SwiftUIApp.swift @@ -22,6 +22,7 @@ class MyAppDelegate: NSObject, NSApplicationDelegate, ObservableObject { options.tracesSampleRate = 1.0 options.profilesSampleRate = 1.0 options.enableUncaughtNSExceptionReporting = true + options.experimental.enableFileManagerSwizzling = true } } diff --git a/Samples/tvOS-Swift/tvOS-SBSwift/AppDelegate.swift b/Samples/tvOS-Swift/tvOS-SBSwift/AppDelegate.swift index ac5bed200db..ed3954d24ce 100644 --- a/Samples/tvOS-Swift/tvOS-SBSwift/AppDelegate.swift +++ b/Samples/tvOS-Swift/tvOS-SBSwift/AppDelegate.swift @@ -12,6 +12,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.sessionTrackingIntervalMillis = 5_000 // Sampling 100% - In Production you probably want to adjust this options.tracesSampleRate = 1.0 + + options.experimental.enableFileManagerSwizzling = true } return true diff --git a/Samples/tvOS-Swift/tvOS-Swift/AppDelegate.swift b/Samples/tvOS-Swift/tvOS-Swift/AppDelegate.swift index 996d7601a09..48ce9966d18 100644 --- a/Samples/tvOS-Swift/tvOS-Swift/AppDelegate.swift +++ b/Samples/tvOS-Swift/tvOS-Swift/AppDelegate.swift @@ -16,6 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Sampling 100% - In Production you probably want to adjust this options.tracesSampleRate = 1.0 options.enableAppHangTracking = true + options.experimental.enableFileManagerSwizzling = true options.initialScope = { scope in if let path = Bundle.main.path(forResource: "Tongariro", ofType: "jpg") { diff --git a/Samples/visionOS-Swift/visionOS-Swift/VisionOSSwiftApp.swift b/Samples/visionOS-Swift/visionOS-Swift/VisionOSSwiftApp.swift index fa9ffadcd82..eb25f009249 100644 --- a/Samples/visionOS-Swift/visionOS-Swift/VisionOSSwiftApp.swift +++ b/Samples/visionOS-Swift/visionOS-Swift/VisionOSSwiftApp.swift @@ -12,6 +12,7 @@ struct VisionOSSwiftApp: App { options.profilesSampleRate = 1.0 options.attachScreenshot = true options.attachViewHierarchy = true + options.experimental.enableFileManagerSwizzling = true } } diff --git a/Samples/watchOS-Swift/watchOS-Swift WatchKit Extension/ExtensionDelegate.swift b/Samples/watchOS-Swift/watchOS-Swift WatchKit Extension/ExtensionDelegate.swift index 5ebe45d1d8b..b1e4c87cd6e 100644 --- a/Samples/watchOS-Swift/watchOS-Swift WatchKit Extension/ExtensionDelegate.swift +++ b/Samples/watchOS-Swift/watchOS-Swift WatchKit Extension/ExtensionDelegate.swift @@ -9,6 +9,7 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { SentrySDK.start { options in options.dsn = "https://6cc9bae94def43cab8444a99e0031c28@o447951.ingest.sentry.io/5428557" options.debug = true + options.experimental.enableFileManagerSwizzling = true } SentrySDK.configureScope { scope in diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 6d60e973ead..8470dd75281 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -35,6 +35,11 @@ - (void)startWithOptions:(SentryOptions *)options initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; [self.dataTracker enable]; + + if (!options.experimental.enableFileManagerSwizzling) { + SENTRY_LOG_DEBUG(@"Experimental auto-tracking of FileManager is disabled") + return; + } [SentryNSFileManagerSwizzling swizzleNSFileManager]; } diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift index b8fbb23f254..a6c97733f40 100644 --- a/Sources/Swift/SentryExperimentalOptions.swift +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -7,6 +7,11 @@ public class SentryExperimentalOptions: NSObject { public var sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 0) #endif + /** + * Enables swizzling of `NSFileManager` + */ + public var enableFileManagerSwizzling = false + func validateOptions(_ options: [String: Any]?) { #if canImport(UIKit) if let sessionReplayOptions = options?["sessionReplay"] as? [String: Any] { From dc0677a73e5e7c193a4b217b0557e9fa2371787b Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Mon, 16 Dec 2024 08:45:32 +0100 Subject: [PATCH 12/32] changed appdelegate configure file length --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 270 ++---------------- 1 file changed, 16 insertions(+), 254 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index c05afe80d78..31fc252d71f 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -6,251 +6,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private var randomDistributionTimer: Timer? var window: UIWindow? - static let defaultDSN = "https://6cc9bae94def43cab8444a99e0031c28@o447951.ingest.sentry.io/5428557" - - //swiftlint:disable function_body_length cyclomatic_complexity - func startSentry() { - let args = ProcessInfo.processInfo.arguments - let env = ProcessInfo.processInfo.environment - - // For testing purposes, we want to be able to change the DSN and store it to disk. In a real app, you shouldn't need this behavior. - var dsn: String? - do { - if let dsn = env["--io.sentry.dsn"] { - try DSNStorage.shared.saveDSN(dsn: dsn) - } - dsn = try DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN - } catch { - print("[iOS-Swift] Error encountered while reading stored DSN: \(error)") - } - - SentrySDK.start(configureOptions: { options in - options.dsn = dsn - options.beforeSend = { event in - return event - } - options.beforeSendSpan = { span in - return span - } - options.beforeCaptureScreenshot = { _ in - return true - } - options.beforeCaptureViewHierarchy = { _ in - return true - } - options.debug = true - - if #available(iOS 16.0, *), !args.contains("--disable-session-replay") { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) - options.experimental.sessionReplay.quality = .high - } - - if #available(iOS 15.0, *), !args.contains("--disable-metrickit-integration") { - options.enableMetricKit = true - options.enableMetricKitRawPayload = true - } - - var tracesSampleRate: NSNumber = 1 - if let tracesSampleRateOverride = env["--io.sentry.tracesSampleRate"] { - tracesSampleRate = NSNumber(value: (tracesSampleRateOverride as NSString).integerValue) - } - options.tracesSampleRate = tracesSampleRate - - if let tracesSamplerValue = env["--io.sentry.tracesSamplerValue"] { - options.tracesSampler = { _ in - return NSNumber(value: (tracesSamplerValue as NSString).integerValue) - } - } - - var profilesSampleRate: NSNumber? = 1 - if args.contains("--io.sentry.enableContinuousProfiling") { - profilesSampleRate = nil - } else if let profilesSampleRateOverride = env["--io.sentry.profilesSampleRate"] { - profilesSampleRate = NSNumber(value: (profilesSampleRateOverride as NSString).integerValue) - } - options.profilesSampleRate = profilesSampleRate - - if let profilesSamplerValue = env["--io.sentry.profilesSamplerValue"] { - options.profilesSampler = { _ in - return NSNumber(value: (profilesSamplerValue as NSString).integerValue) - } - } - - options.enableAppLaunchProfiling = args.contains("--profile-app-launches") - - options.enableAutoSessionTracking = !args.contains("--disable-automatic-session-tracking") - if let sessionTrackingIntervalMillis = env["--io.sentry.sessionTrackingIntervalMillis"] { - options.sessionTrackingIntervalMillis = UInt((sessionTrackingIntervalMillis as NSString).integerValue) - } - options.attachScreenshot = true - options.attachViewHierarchy = true - -#if targetEnvironment(simulator) - options.enableSpotlight = !args.contains("--disable-spotlight") -#else - options.enableWatchdogTerminationTracking = false // The UI tests generate false OOMs -#endif - options.enableTimeToFullDisplayTracing = true - options.enablePerformanceV2 = true - - options.add(inAppInclude: "iOS_External") - - let isBenchmarking = args.contains("--io.sentry.test.benchmarking") - - // the benchmark test starts and stops a custom transaction using a UIButton, and automatic user interaction tracing stops the transaction that begins with that button press after the idle timeout elapses, stopping the profiler (only one profiler runs regardless of the number of concurrent transactions) - options.enableUserInteractionTracing = !isBenchmarking && !args.contains("--disable-ui-tracing") - options.enableAutoPerformanceTracing = !isBenchmarking && !args.contains("--disable-auto-performance-tracing") - options.enablePreWarmedAppStartTracing = !isBenchmarking - - options.enableFileIOTracing = !args.contains("--disable-file-io-tracing") - options.enableAutoBreadcrumbTracking = !args.contains("--disable-automatic-breadcrumbs") - options.enableUIViewControllerTracing = !args.contains("--disable-uiviewcontroller-tracing") - options.enableNetworkTracking = !args.contains("--disable-network-tracking") - options.enableCoreDataTracing = !args.contains("--disable-core-data-tracing") - options.enableNetworkBreadcrumbs = !args.contains("--disable-network-breadcrumbs") - options.enableSwizzling = !args.contains("--disable-swizzling") - options.experimental.enableFileManagerSwizzling = !args.contains("--disable-filemanager-swizzling") - options.enableCrashHandler = !args.contains("--disable-crash-handler") - options.enableTracing = !args.contains("--disable-tracing") - options.enablePersistingTracesWhenCrashing = true - - // because we run CPU for 15 seconds at full throttle, we trigger ANR issues being sent. disable such during benchmarks. - options.enableAppHangTracking = !isBenchmarking && !args.contains("--disable-anr-tracking") - options.enableWatchdogTerminationTracking = !isBenchmarking && !args.contains("--disable-watchdog-tracking") - options.appHangTimeoutInterval = 2 - options.enableCaptureFailedRequests = true - let httpStatusCodeRange = HttpStatusCodeRange(min: 400, max: 599) - options.failedRequestStatusCodes = [ httpStatusCodeRange ] - options.beforeBreadcrumb = { breadcrumb in - //Raising notifications when a new breadcrumb is created in order to use this information - //to validate whether proper breadcrumb are being created in the right places. - NotificationCenter.default.post(name: .init("io.sentry.newbreadcrumb"), object: breadcrumb) - return breadcrumb - } - - options.initialScope = { scope in - if let environmentOverride = env["--io.sentry.sdk-environment"] { - scope.setEnvironment(environmentOverride) - } else if isBenchmarking { - scope.setEnvironment("benchmarking") - } else { - #if targetEnvironment(simulator) - scope.setEnvironment("simulator") - #else - scope.setEnvironment("device") - #endif // targetEnvironment(simulator) - } - - scope.setTag(value: "swift", key: "language") - - scope.injectGitInformation() - - let user = User(userId: "1") - user.email = env["--io.sentry.user.email"] ?? "tony@example.com" - // first check if the username has been overridden in the scheme for testing purposes; then try to use the system username so each person gets an automatic way to easily filter things on the dashboard; then fall back on a hardcoded value if none of these are present - let username = env["--io.sentry.user.username"] ?? (env["SIMULATOR_HOST_HOME"] as? NSString)? - .lastPathComponent ?? "cocoa developer" - user.username = username - scope.setUser(user) - - if let path = Bundle.main.path(forResource: "Tongariro", ofType: "jpg") { - scope.addAttachment(Attachment(path: path, filename: "Tongariro.jpg", contentType: "image/jpeg")) - } - if let data = "hello".data(using: .utf8) { - scope.addAttachment(Attachment(data: data, filename: "log.txt")) - } - return scope - } - - options.configureUserFeedback = { config in - let layoutOffset = UIOffset(horizontal: 25, vertical: 75) - guard !args.contains("--io.sentry.feedback.all-defaults") else { - config.configureWidget = { widget in - widget.layoutUIOffset = layoutOffset - } - return - } - config.animations = !args.contains("--io.sentry.feedback.no-animations") - config.useShakeGesture = true - config.showFormForScreenshots = true - config.configureWidget = { widget in - if args.contains("--io.sentry.feedback.auto-inject-widget") { - if Locale.current.languageCode == "ar" { // arabic - widget.labelText = "﷽" - } else if Locale.current.languageCode == "ur" { // urdu - widget.labelText = "نستعلیق" - } else if Locale.current.languageCode == "he" { // hebrew - widget.labelText = "עִבְרִית‎" - } else if Locale.current.languageCode == "hi" { // Hindi - widget.labelText = "नागरि" - } else { - widget.labelText = "Report Jank" - } - widget.widgetAccessibilityLabel = "io.sentry.iOS-Swift.button.report-jank" - widget.layoutUIOffset = layoutOffset - } else { - widget.autoInject = false - } - if args.contains("--io.sentry.feedback.no-widget-text") { - widget.labelText = nil - } - if args.contains("--io.sentry.feedback.no-widget-icon") { - widget.showIcon = false - } - } - config.configureForm = { uiForm in - uiForm.formTitle = "Jank Report" - uiForm.isEmailRequired = args.contains("--io.sentry.feedback.require-email") - uiForm.isNameRequired = args.contains("--io.sentry.feedback.require-name") - uiForm.submitButtonLabel = "Report that jank" - uiForm.addScreenshotButtonLabel = "Show us the jank" - uiForm.removeScreenshotButtonLabel = "Oof too nsfl" - uiForm.cancelButtonLabel = "What, me worry?" - uiForm.messagePlaceholder = "Describe the nature of the jank. Its essence, if you will." - uiForm.namePlaceholder = "Yo name" - uiForm.emailPlaceholder = "Yo email" - uiForm.messageLabel = "Thy complaint" - uiForm.emailLabel = "Thine email" - uiForm.nameLabel = "Thy name" - } - config.configureTheme = { theme in - let fontFamily: String - if Locale.current.languageCode == "ar" { // arabic; ar_EG - fontFamily = "Damascus" - } else if Locale.current.languageCode == "ur" { // urdu; ur_PK - fontFamily = "NotoNastaliq" - } else if Locale.current.languageCode == "he" { // hebrew; he_IL - fontFamily = "Arial Hebrew" - } else if Locale.current.languageCode == "hi" { // Hindi; hi_IN - fontFamily = "DevanagariSangamMN" - } else { - fontFamily = "ChalkboardSE-Regular" - } - theme.fontFamily = fontFamily - theme.outlineStyle = .init(outlineColor: .purple) - theme.foreground = .purple - theme.background = .init(red: 0.95, green: 0.9, blue: 0.95, alpha: 1) - theme.submitBackground = .orange - theme.submitForeground = .purple - theme.buttonBackground = .purple - theme.buttonForeground = .white - } - config.onSubmitSuccess = { info in - let name = info["name"] ?? "$shakespearean_insult_name" - let alert = UIAlertController(title: "Thanks?", message: "We have enough jank of our own, we really didn't need yours too, \(name).", preferredStyle: .alert) - alert.addAction(.init(title: "Deal with it 🕶️", style: .default)) - self.window?.rootViewController?.present(alert, animated: true) - } - config.onSubmitError = { error in - let alert = UIAlertController(title: "D'oh", message: "You tried to report jank, and encountered more jank. The jank has you now: \(error).", preferredStyle: .alert) - alert.addAction(.init(title: "Derp", style: .default)) - self.window?.rootViewController?.present(alert, animated: true) - } - } - }) - } - //swiftlint:enable function_body_length cyclomatic_complexity - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if args.contains("--io.sentry.wipe-data") { removeAppData() @@ -320,15 +75,8 @@ extension AppDelegate { options.beforeCaptureViewHierarchy = { _ in true } options.debug = true - if #available(iOS 16.0, *), enableSessionReplay { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) - options.experimental.sessionReplay.quality = .high - } - - if #available(iOS 15.0, *), enableMetricKit { - options.enableMetricKit = true - options.enableMetricKitRawPayload = true - } + configureSessionReplay(options: options) + configureMetricKit(options: options) options.tracesSampleRate = tracesSampleRate options.tracesSampler = tracesSampler @@ -375,6 +123,20 @@ extension AppDelegate { options.initialScope = configureInitialScope(scope:) options.configureUserFeedback = configureFeedback(config:) } + + func configureSessionReplay(options: Options) { + if #available(iOS 16.0, *), enableSessionReplay { + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, onErrorSampleRate: 1, maskAllText: true, maskAllImages: true) + options.experimental.sessionReplay.quality = .high + } + } + + func configureMetricKit(options: Options) { + if #available(iOS 15.0, *), enableMetricKit { + options.enableMetricKit = true + options.enableMetricKitRawPayload = true + } + } func configureInitialScope(scope: Scope) -> Scope { if let environmentOverride = self.env["--io.sentry.sdk-environment"] { From 9055b19ccdb30d36ece6a47081a5e002b03afd30 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 09:51:20 +0100 Subject: [PATCH 13/32] add more context to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b79b1f352..97ed6786829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Fixes -- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18 (#4634) +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18. This feature is experimental and must be enabled by setting the option `experimental.enableFileManagerSwizzling` to `true` (#4634) - `SentrySdkInfo.packages` should be an array (#4626) ### Internal From b23f274d24d6cd907a27db75a07f4a7d8ef25470 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 09:57:28 +0100 Subject: [PATCH 14/32] add span validation to testCreateFile --- .../Performance/IO/SentryFileIOTrackerTests.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift index ff8b2cda5a2..72e1ff5c1a0 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackerTests.swift @@ -277,21 +277,33 @@ class SentryFileIOTrackerTests: XCTestCase { func testCreateFile() { let sut = fixture.getSut() + var methodPath: String? var methodData: Data? var methodAttributes: [FileAttributeKey: Any]? - + + let transaction = SentrySDK.startTransaction(name: "Transaction", operation: "Test", bindToScope: true) + var span: Span? + sut.measureNSFileManagerCreateFile(atPath: fixture.filePath, data: fixture.data, attributes: [ FileAttributeKey.size: 123 ], method: { path, data, attributes in methodPath = path methodData = data methodAttributes = attributes + + span = self.firstSpan(transaction) + XCTAssertFalse(span?.isFinished ?? true) + self.advanceTime(bySeconds: 4) + return true }) XCTAssertEqual(methodPath, fixture.filePath) XCTAssertEqual(methodData, fixture.data) 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) } func testDontTrackSentryFilesRead() { From 7d0ccd3bbced23aee9b357be6669af23b3cda199 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 10:00:55 +0100 Subject: [PATCH 15/32] add ref to Swift-Foundation PR --- Sources/Sentry/SentryNSFileManagerSwizzling.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 8470dd75281..888fe35532d 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -54,9 +54,11 @@ - (void)stop #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. + // Before iOS 18.0, macOS 15.0 and tvOS 18.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. + // + // Ref: https://github.com/swiftlang/swift-foundation/pull/410 if (@available(iOS 18, macOS 15, *)) { SEL createFileAtPathContentsAttributes = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); From 4ab353ce00fa03fcfb1f6cb4b8cf75be36881df9 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 10:01:29 +0100 Subject: [PATCH 16/32] add availability check for tvOS 18 --- Sources/Sentry/SentryNSFileManagerSwizzling.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 888fe35532d..cce4b143219 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -59,7 +59,7 @@ + (void)swizzleNSFileManager // the file manager needs to swizzled for later versions. // // Ref: https://github.com/swiftlang/swift-foundation/pull/410 - if (@available(iOS 18, macOS 15, *)) { + if (@available(iOS 18, macOS 15, tvOS 18, *)) { SEL createFileAtPathContentsAttributes = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); SentrySwizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, From 458e90355b6a4b44a8ba94deac70ed411b965f6d Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 11:07:12 +0100 Subject: [PATCH 17/32] add unit test for NSFileManager swizzling --- Sentry.xcodeproj/project.pbxproj | 4 + ...SentryFileIOTrackingIntegrationObjCTests.m | 8 +- .../IO/SentryNSFileManagerSwizzlingTests.m | 187 ++++++++++++++++++ 3 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 7d743b68a69..427cffbc4c4 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -782,6 +782,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 */; }; + D45588682D117BD9002B24C0 /* SentryNSFileManagerSwizzlingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D45588672D117BD9002B24C0 /* SentryNSFileManagerSwizzlingTests.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 */; }; @@ -1861,6 +1862,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 = ""; }; + D45588672D117BD9002B24C0 /* SentryNSFileManagerSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSFileManagerSwizzlingTests.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 = ""; }; @@ -3811,6 +3813,7 @@ isa = PBXGroup; children = ( D875ED0A276CC84700422FAC /* SentryFileIOTrackerTests.swift */, + D45588672D117BD9002B24C0 /* SentryNSFileManagerSwizzlingTests.m */, D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */, D8CE69BB277E39C700C6EC5C /* SentryFileIOTrackingIntegrationObjCTests.m */, 7B82722A27A3220A00F4BFF4 /* SentryFileIoTrackingUnitTests.swift */, @@ -4911,6 +4914,7 @@ 7B98D7EC25FB7C4900C5A389 /* SentryAppStateTests.swift in Sources */, 62A3C7BE2B7E2A6A00C75227 /* SentrySpotlightTransportTests.swift in Sources */, 8EE017A126704CD500470616 /* SentryUIViewControllerPerformanceTrackerTests.swift in Sources */, + D45588682D117BD9002B24C0 /* SentryNSFileManagerSwizzlingTests.m in Sources */, 7B5B94352657AD21002E474B /* SentryFramesTrackingIntegrationTests.swift in Sources */, 8431EE5B29ADB8EA00D8DC56 /* SentryTimeTests.m in Sources */, 7B0A54562523178700A71716 /* SentryScopeSwiftTests.swift in Sources */, diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m index fb1facfd94b..b2dcb667453 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationObjCTests.m @@ -5,6 +5,7 @@ #import "SentrySpan.h" #import "SentrySwizzle.h" #import "SentryTracer.h" +#import #import @interface SentryFileIOTrackingIntegrationObjCTests : XCTestCase @@ -53,6 +54,8 @@ - (void)setUp options.enableAutoPerformanceTracing = YES; options.enableFileIOTracing = YES; options.tracesSampleRate = @1; + + options.experimental.enableFileManagerSwizzling = YES; }]; } @@ -177,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:SENTRY_FILE_WRITE_OPERATION block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m new file mode 100644 index 00000000000..b0b61cdf797 --- /dev/null +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -0,0 +1,187 @@ +#import "SentryByteCountFormatter.h" +#import "SentryFileIOTracker.h" +#import "SentryOptions.h" +#import "SentrySDK.h" +#import "SentrySpan.h" +#import "SentrySwizzle.h" +#import "SentryTracer.h" +#import +#import + +@interface SentryNSFileManagerSwizzlingTests : XCTestCase + +@end + +@implementation SentryNSFileManagerSwizzlingTests { + NSString *filePath; + NSURL *fileUrl; + NSData *someData; + NSURL *fileDirectory; + BOOL deleteFileDirectory; +} + +- (void)inititialize +{ + NSArray *directories = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory + inDomains:NSUserDomainMask]; + fileDirectory = directories.firstObject; + + if (![NSFileManager.defaultManager fileExistsAtPath:fileDirectory.path]) { + deleteFileDirectory = true; + [NSFileManager.defaultManager createDirectoryAtURL:fileDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + + fileUrl = [fileDirectory URLByAppendingPathComponent:@"TestFile"]; + filePath = fileUrl.path; +} + +- (void)setUp +{ + [super setUp]; + [self inititialize]; + + someData = [@"SOME DATA" dataUsingEncoding:NSUTF8StringEncoding]; + [someData writeToFile:filePath atomically:true]; +} + +- (void)setUpSentrySDKwithEnableFileManagerSwizzling:(bool)enableFileManagerSwizzling +{ + [SentrySDK startWithConfigureOptions:^(SentryOptions *_Nonnull options) { + options.enableAutoPerformanceTracing = YES; + options.enableFileIOTracing = YES; + options.tracesSampleRate = @1; + + options.experimental.enableFileManagerSwizzling = enableFileManagerSwizzling; + }]; +} + +- (void)tearDown +{ + [NSFileManager.defaultManager removeItemAtURL:fileUrl error:nil]; + if (deleteFileDirectory) { + [NSFileManager.defaultManager removeItemAtURL:fileDirectory error:nil]; + } + [SentrySDK close]; +} + +- (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabled_shouldNotSwizzle +{ + if (@available(iOS 18, macOS 15, tvOS 18, *)) { + XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); + } + [self setUpSentrySDKwithEnableFileManagerSwizzling:NO]; + // Pre iOS 18, macOS 15, tvOS 18 the NSFileManager uses NSData, which is swizzled. + // As NSData swizzling can not be disabled, it will still record a span + [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + spanCount:1 + block:^{ + [NSFileManager.defaultManager createFileAtPath:self->filePath + contents:self->someData + attributes:nil]; + }]; + [self assertDataWritten]; +} + +- (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnabled_shouldNotSwizzle +{ + if (@available(iOS 18, macOS 15, tvOS 18, *)) { + XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); + } + [self setUpSentrySDKwithEnableFileManagerSwizzling:YES]; + // Pre iOS 18, macOS 15, tvOS 18 the NSFileManager uses NSData, which is swizzled. + // As NSData swizzling can not be disabled, it will still record a span + [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + spanCount:1 + block:^{ + [NSFileManager.defaultManager createFileAtPath:self->filePath + contents:self->someData + attributes:nil]; + }]; + [self assertDataWritten]; +} + +- (void) + testNSFileManagerCreateFile_iOS18macOS15tvOS18OrLater_experimentalFlagDisabled_shouldNotSwizzle +{ + if (@available(iOS 18, macOS 15, tvOS 18, *)) { + // continue + } else { + XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); + } + [self setUpSentrySDKwithEnableFileManagerSwizzling:NO]; + [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + spanCount:0 + block:^{ + [NSFileManager.defaultManager createFileAtPath:self->filePath + contents:self->someData + attributes:nil]; + }]; + [self assertDataWritten]; +} + +- (void)testNSFileManagerCreateFile_iOS18macOS15tvOS18OrLater_experimentalFlagEnabled_shouldSwizzle +{ + if (@available(iOS 18, macOS 15, tvOS 18, *)) { + // continue + } else { + XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); + } + [self setUpSentrySDKwithEnableFileManagerSwizzling:YES]; + [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION + spanCount:1 + block:^{ + [NSFileManager.defaultManager createFileAtPath:self->filePath + contents:self->someData + attributes:nil]; + }]; + [self assertDataWritten]; +} + +- (void)assertDataWritten +{ + [self assertData:[NSData dataWithContentsOfFile:filePath]]; +} + +- (void)assertData:(NSData *)data +{ + NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + XCTAssertEqualObjects(content, @"SOME DATA"); + XCTAssertEqual(data.length, someData.length); +} + +- (void)assertTransactionForOperation:(NSString *)operation + spanCount:(NSUInteger)spanCount + block:(void (^)(void))block +{ + SentryTracer *parentTransaction = [SentrySDK startTransactionWithName:@"Transaction" + operation:@"Test" + bindToScope:YES]; + + block(); + + XCTAssertEqual(parentTransaction.children.count, spanCount); + + SentrySpan *ioSpan = parentTransaction.children.firstObject; + if (spanCount > 0) { + XCTAssertEqual([ioSpan.data[@"file.size"] unsignedIntValue], someData.length); + XCTAssertEqualObjects(ioSpan.data[@"file.path"], filePath); + XCTAssertEqualObjects(operation, ioSpan.operation); + + NSString *filename = filePath.lastPathComponent; + + if ([operation isEqualToString:SENTRY_FILE_READ_OPERATION]) { + XCTAssertEqualObjects(ioSpan.spanDescription, filename); + } else { + NSString *expectedString = [NSString stringWithFormat:@"%@ (%@)", filename, + [SentryByteCountFormatter bytesCountDescription:someData.length]]; + + XCTAssertEqualObjects(ioSpan.spanDescription, expectedString); + } + } +} + +@end From 59c24db0cc30158d1cf2bf5999eeeb0ae7b860dc Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 17 Dec 2024 11:14:43 +0100 Subject: [PATCH 18/32] refactor filemanager swizzling tests --- .../IO/SentryNSFileManagerSwizzlingTests.m | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index b0b61cdf797..e6bfb99beb3 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -1,5 +1,6 @@ #import "SentryByteCountFormatter.h" #import "SentryFileIOTracker.h" +#import "SentryNSFileManagerSwizzling.h" #import "SentryOptions.h" #import "SentrySDK.h" #import "SentrySpan.h" @@ -47,15 +48,11 @@ - (void)setUp [someData writeToFile:filePath atomically:true]; } -- (void)setUpSentrySDKwithEnableFileManagerSwizzling:(bool)enableFileManagerSwizzling +- (void)setUpNSFileManagerSwizzlingWithEnabledFlag:(bool)enableFileManagerSwizzling { - [SentrySDK startWithConfigureOptions:^(SentryOptions *_Nonnull options) { - options.enableAutoPerformanceTracing = YES; - options.enableFileIOTracing = YES; - options.tracesSampleRate = @1; - - options.experimental.enableFileManagerSwizzling = enableFileManagerSwizzling; - }]; + SentryOptions *options = [[SentryOptions alloc] init]; + options.experimental.enableFileManagerSwizzling = enableFileManagerSwizzling; + [[SentryNSFileManagerSwizzling shared] startWithOptions:options]; } - (void)tearDown @@ -64,7 +61,7 @@ - (void)tearDown if (deleteFileDirectory) { [NSFileManager.defaultManager removeItemAtURL:fileDirectory error:nil]; } - [SentrySDK close]; + [[SentryNSFileManagerSwizzling shared] stop]; } - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabled_shouldNotSwizzle @@ -72,11 +69,9 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabl if (@available(iOS 18, macOS 15, tvOS 18, *)) { XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } - [self setUpSentrySDKwithEnableFileManagerSwizzling:NO]; - // Pre iOS 18, macOS 15, tvOS 18 the NSFileManager uses NSData, which is swizzled. - // As NSData swizzling can not be disabled, it will still record a span + [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION - spanCount:1 + spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath contents:self->someData @@ -90,11 +85,9 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable if (@available(iOS 18, macOS 15, tvOS 18, *)) { XCTSkip("Test only targets pre iOS 18, macOS 15, tvOS 18"); } - [self setUpSentrySDKwithEnableFileManagerSwizzling:YES]; - // Pre iOS 18, macOS 15, tvOS 18 the NSFileManager uses NSData, which is swizzled. - // As NSData swizzling can not be disabled, it will still record a span + [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION - spanCount:1 + spanCount:0 block:^{ [NSFileManager.defaultManager createFileAtPath:self->filePath contents:self->someData @@ -111,7 +104,7 @@ - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagEnable } else { XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } - [self setUpSentrySDKwithEnableFileManagerSwizzling:NO]; + [self setUpNSFileManagerSwizzlingWithEnabledFlag:NO]; [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION spanCount:0 block:^{ @@ -129,7 +122,7 @@ - (void)testNSFileManagerCreateFile_iOS18macOS15tvOS18OrLater_experimentalFlagEn } else { XCTSkip("Test only targets iOS 18, macOS 15, tvOS 18 or later"); } - [self setUpSentrySDKwithEnableFileManagerSwizzling:YES]; + [self setUpNSFileManagerSwizzlingWithEnabledFlag:YES]; [self assertTransactionForOperation:SENTRY_FILE_WRITE_OPERATION spanCount:1 block:^{ From a11ba03fa9c1c14bae523e35a7c853d64206c500 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 18 Dec 2024 11:54:16 +0100 Subject: [PATCH 19/32] refactor swizzling to share tracker instance --- .../Sentry/SentryFileIOTrackingIntegration.m | 23 ++++++++--- Sources/Sentry/SentryNSDataSwizzling.m | 39 ++++++------------- Sources/Sentry/SentryNSFileManagerSwizzling.m | 31 ++++----------- .../Sentry/include/SentryNSDataSwizzling.h | 5 +-- .../include/SentryNSFileManagerSwizzling.h | 5 +-- .../IO/SentryNSFileManagerSwizzlingTests.m | 15 ++++++- scripts/.clang-format-version | 2 +- 7 files changed, 53 insertions(+), 67 deletions(-) diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index 09a04dd3797..adc8629d4e5 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -1,8 +1,15 @@ #import "SentryFileIOTrackingIntegration.h" -#import "SentryLog.h" +#import "SentryDependencyContainer.h" +#import "SentryFileIOTracker.h" #import "SentryNSDataSwizzling.h" #import "SentryNSFileManagerSwizzling.h" -#import "SentryOptions.h" +#import "SentryThreadInspector.h" + +@interface SentryFileIOTrackingIntegration () + +@property (nonatomic, strong) SentryFileIOTracker *tracker; + +@end @implementation SentryFileIOTrackingIntegration @@ -12,8 +19,13 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } - [SentryNSDataSwizzling.shared startWithOptions:options]; - [SentryNSFileManagerSwizzling.shared startWithOptions:options]; + self.tracker = [[SentryFileIOTracker alloc] + initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] + processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; + [self.tracker enable]; + + [SentryNSDataSwizzling.shared startWithOptions:options tracker:self.tracker]; + [SentryNSFileManagerSwizzling.shared startWithOptions:options tracker:self.tracker]; return YES; } @@ -26,8 +38,7 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { - [SentryNSDataSwizzling.shared stop]; - [SentryNSFileManagerSwizzling.shared stop]; + [self.tracker disable]; } @end diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index c7ab31f0779..312ca92efc0 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -1,21 +1,11 @@ #import "SentryNSDataSwizzling.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 "SentryLog.h" #import "SentrySwizzle.h" -#import "SentryThreadInspector.h" -#import #import @interface SentryNSDataSwizzling () -@property (nonatomic, strong) SentryFileIOTracker *dataTracker; +@property (nonatomic, strong) SentryFileIOTracker *tracker; @end @@ -29,31 +19,24 @@ + (SentryNSDataSwizzling *)shared return instance; } -- (void)startWithOptions:(SentryOptions *)options +- (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker { - self.dataTracker = [[SentryFileIOTracker alloc] - initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] - processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; - [self.dataTracker enable]; - [SentryNSDataSwizzling swizzleNSData]; -} + self.tracker = tracker; -- (void)stop -{ - [self.dataTracker disable]; + [SentryNSDataSwizzling swizzle]; } // 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)swizzleNSData ++ (void)swizzle { SEL writeToFileAtomicallySelector = NSSelectorFromString(@"writeToFile:atomically:"); SentrySwizzleInstanceMethod(NSData.class, writeToFileAtomicallySelector, SentrySWReturnType(BOOL), SentrySWArguments(NSString * path, BOOL useAuxiliaryFile), SentrySWReplacement({ - return [SentryNSDataSwizzling.shared.dataTracker + return [SentryNSDataSwizzling.shared.tracker measureNSData:self writeToFile:path atomically:useAuxiliaryFile @@ -68,7 +51,7 @@ + (void)swizzleNSData SentrySWReturnType(BOOL), SentrySWArguments(NSString * path, NSDataWritingOptions writeOptionsMask, NSError * *error), SentrySWReplacement({ - return [SentryNSDataSwizzling.shared.dataTracker + return [SentryNSDataSwizzling.shared.tracker measureNSData:self writeToFile:path options:writeOptionsMask @@ -86,7 +69,7 @@ + (void)swizzleNSData SentrySWReturnType(NSData *), SentrySWArguments(NSString * path, NSDataReadingOptions options, NSError * *error), SentrySWReplacement({ - return [SentryNSDataSwizzling.shared.dataTracker + return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path options:options error:error @@ -101,7 +84,7 @@ + (void)swizzleNSData SEL initWithContentsOfFileSelector = NSSelectorFromString(@"initWithContentsOfFile:"); SentrySwizzleInstanceMethod(NSData.class, initWithContentsOfFileSelector, SentrySWReturnType(NSData *), SentrySWArguments(NSString * path), SentrySWReplacement({ - return [SentryNSDataSwizzling.shared.dataTracker + return [SentryNSDataSwizzling.shared.tracker measureNSDataFromFile:path method:^NSData *( NSString *filePath) { return SentrySWCallOriginal(filePath); }]; @@ -114,7 +97,7 @@ + (void)swizzleNSData SentrySWReturnType(NSData *), SentrySWArguments(NSURL * url, NSDataReadingOptions options, NSError * *error), SentrySWReplacement({ - return [SentryNSDataSwizzling.shared.dataTracker + return [SentryNSDataSwizzling.shared.tracker measureNSDataFromURL:url options:options error:error diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index cce4b143219..a1d622cbb55 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -1,21 +1,11 @@ #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 "SentryLog.h" #import "SentrySwizzle.h" -#import "SentryThreadInspector.h" -#import #import @interface SentryNSFileManagerSwizzling () -@property (nonatomic, strong) SentryFileIOTracker *dataTracker; +@property (nonatomic, strong) SentryFileIOTracker *tracker; @end @@ -29,30 +19,23 @@ + (SentryNSFileManagerSwizzling *)shared return instance; } -- (void)startWithOptions:(SentryOptions *)options +- (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker { - self.dataTracker = [[SentryFileIOTracker alloc] - initWithThreadInspector:[[SentryThreadInspector alloc] initWithOptions:options] - processInfoWrapper:[SentryDependencyContainer.sharedInstance processInfoWrapper]]; - [self.dataTracker enable]; + self.tracker = tracker; if (!options.experimental.enableFileManagerSwizzling) { SENTRY_LOG_DEBUG(@"Experimental auto-tracking of FileManager is disabled") return; } - [SentryNSFileManagerSwizzling swizzleNSFileManager]; -} -- (void)stop -{ - [self.dataTracker disable]; + [SentryNSFileManagerSwizzling swizzle]; } // 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 ++ (void)swizzle { // Before iOS 18.0, macOS 15.0 and tvOS 18.0 the NSFileManager used NSData.writeToFile // internally, which was tracked using swizzling of NSData. This behaviour changed, therefore @@ -67,7 +50,7 @@ + (void)swizzleNSFileManager SentrySWArguments( NSString * path, NSData * data, NSDictionary * attributes), SentrySWReplacement({ - return [SentryNSFileManagerSwizzling.shared.dataTracker + return [SentryNSFileManagerSwizzling.shared.tracker measureNSFileManagerCreateFileAtPath:path data:data attributes:attributes diff --git a/Sources/Sentry/include/SentryNSDataSwizzling.h b/Sources/Sentry/include/SentryNSDataSwizzling.h index 4c041da813c..8b828f48806 100644 --- a/Sources/Sentry/include/SentryNSDataSwizzling.h +++ b/Sources/Sentry/include/SentryNSDataSwizzling.h @@ -1,4 +1,5 @@ #import "SentryDefines.h" +#import "SentryFileIOTracker.h" #import NS_ASSUME_NONNULL_BEGIN @@ -10,9 +11,7 @@ SENTRY_NO_INIT @property (class, readonly) SentryNSDataSwizzling *shared; -- (void)startWithOptions:(SentryOptions *)options; - -- (void)stop; +- (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker; @end diff --git a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h index cbc91a9190b..b9e3c1dad35 100644 --- a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h +++ b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h @@ -1,4 +1,5 @@ #import "SentryDefines.h" +#import "SentryFileIOTracker.h" #import NS_ASSUME_NONNULL_BEGIN @@ -10,9 +11,7 @@ SENTRY_NO_INIT @property (class, readonly) SentryNSFileManagerSwizzling *shared; -- (void)startWithOptions:(SentryOptions *)options; - -- (void)stop; +- (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker; @end diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index e6bfb99beb3..d2ffc50af5d 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -5,6 +5,7 @@ #import "SentrySDK.h" #import "SentrySpan.h" #import "SentrySwizzle.h" +#import "SentryThreadInspector.h" #import "SentryTracer.h" #import #import @@ -19,6 +20,7 @@ @implementation SentryNSFileManagerSwizzlingTests { NSData *someData; NSURL *fileDirectory; BOOL deleteFileDirectory; + SentryFileIOTracker *tracker; } - (void)inititialize @@ -52,7 +54,16 @@ - (void)setUpNSFileManagerSwizzlingWithEnabledFlag:(bool)enableFileManagerSwizzl { SentryOptions *options = [[SentryOptions alloc] init]; options.experimental.enableFileManagerSwizzling = enableFileManagerSwizzling; - [[SentryNSFileManagerSwizzling shared] startWithOptions:options]; + + SentryThreadInspector *threadInspector = + [[SentryThreadInspector alloc] initWithOptions:options]; + SentryNSProcessInfoWrapper *processInfoWrapper = + [SentryDependencyContainer.sharedInstance processInfoWrapper]; + self->tracker = [[SentryFileIOTracker alloc] initWithThreadInspector:threadInspector + processInfoWrapper:processInfoWrapper]; + [tracker enable]; + + [[SentryNSFileManagerSwizzling shared] startWithOptions:options tracker:self->tracker]; } - (void)tearDown @@ -61,7 +72,7 @@ - (void)tearDown if (deleteFileDirectory) { [NSFileManager.defaultManager removeItemAtURL:fileDirectory error:nil]; } - [[SentryNSFileManagerSwizzling shared] stop]; + [self->tracker disable]; } - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabled_shouldNotSwizzle 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 872f7bc0aff8630ac011ad38eb1c78e3020c95ce Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 19 Dec 2024 10:48:11 +0100 Subject: [PATCH 20/32] add unswizzling --- .../Sentry/SentryFileIOTrackingIntegration.m | 3 + Sources/Sentry/SentryNSDataSwizzling.m | 30 ++++++ Sources/Sentry/SentryNSFileManagerSwizzling.m | 15 +++ Sources/Sentry/SentrySwizzle.m | 92 ++++++++++++++++++- .../include/HybridPublic/SentrySwizzle.h | 29 ++++++ .../Sentry/include/SentryNSDataSwizzling.h | 2 + .../include/SentryNSFileManagerSwizzling.h | 2 + Tests/SentryTests/Helper/SentrySwizzleTests.m | 23 +++++ ...SentryFileIOTrackingIntegrationTests.swift | 1 + .../IO/SentryNSFileManagerSwizzlingTests.m | 1 + 10 files changed, 196 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentryFileIOTrackingIntegration.m b/Sources/Sentry/SentryFileIOTrackingIntegration.m index adc8629d4e5..5c650ca7ce4 100644 --- a/Sources/Sentry/SentryFileIOTrackingIntegration.m +++ b/Sources/Sentry/SentryFileIOTrackingIntegration.m @@ -39,6 +39,9 @@ - (SentryIntegrationOption)integrationOptions - (void)uninstall { [self.tracker disable]; + + [SentryNSDataSwizzling.shared stop]; + [SentryNSFileManagerSwizzling.shared stop]; } @end diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 312ca92efc0..79d1d182583 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -26,6 +26,11 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * [SentryNSDataSwizzling swizzle]; } +- (void)stop +{ + [SentryNSDataSwizzling unswizzle]; +} + // SentrySwizzleInstanceMethod declaration shadows a local variable. The swizzling is working // fine and we accept this warning. #pragma clang diagnostic push @@ -109,5 +114,30 @@ + (void)swizzle SentrySwizzleModeOncePerClassAndSuperclasses, (void *)initWithContentsOfURLOptionsErrorSelector); } + ++ (void)unswizzle +{ + SEL writeToFileAtomicallySelector = NSSelectorFromString(@"writeToFile:atomically:"); + SentryUnswizzleInstanceMethod( + NSData.class, writeToFileAtomicallySelector, (void *)writeToFileAtomicallySelector); + + SEL writeToFileOptionsErrorSelector = NSSelectorFromString(@"writeToFile:options:error:"); + SentryUnswizzleInstanceMethod( + NSData.class, writeToFileOptionsErrorSelector, (void *)writeToFileOptionsErrorSelector); + + SEL initWithContentOfFileOptionsErrorSelector + = NSSelectorFromString(@"initWithContentsOfFile:options:error:"); + SentryUnswizzleInstanceMethod(NSData.class, initWithContentOfFileOptionsErrorSelector, + (void *)initWithContentOfFileOptionsErrorSelector); + + SEL initWithContentsOfFileSelector = NSSelectorFromString(@"initWithContentsOfFile:"); + SentryUnswizzleInstanceMethod( + NSData.class, initWithContentsOfFileSelector, (void *)initWithContentsOfFileSelector); + + SEL initWithContentsOfURLOptionsErrorSelector + = NSSelectorFromString(@"initWithContentsOfURL:options:error:"); + SentryUnswizzleInstanceMethod(NSData.class, initWithContentsOfURLOptionsErrorSelector, + (void *)initWithContentsOfURLOptionsErrorSelector); +} #pragma clang diagnostic pop @end diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index a1d622cbb55..f00fe9e1e2d 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -31,6 +31,11 @@ - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker * [SentryNSFileManagerSwizzling swizzle]; } +- (void)stop +{ + [SentryNSFileManagerSwizzling unswizzle]; +} + // SentrySwizzleInstanceMethod declaration shadows a local variable. The swizzling is working // fine and we accept this warning. #pragma clang diagnostic push @@ -65,5 +70,15 @@ + (void)swizzle (void *)createFileAtPathContentsAttributes); } } + ++ (void)unswizzle +{ + if (@available(iOS 18, macOS 15, tvOS 18, *)) { + SEL createFileAtPathContentsAttributes + = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); + SentryUnswizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, + (void *)createFileAtPathContentsAttributes); + } +} #pragma clang diagnostic pop @end diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 13c37553557..6a37ee95ee9 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -40,8 +40,46 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +static NSMutableDictionary * +refsToOriginalImplementationsDictionary(void) +{ + static NSMutableDictionary *refsToOriginalImplementations; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ refsToOriginalImplementations = [NSMutableDictionary new]; }); + return refsToOriginalImplementations; +} + +static void +storeRefToOriginalImplementation(const void *key, IMP implementation) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation]; +} + +static void +removeRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + [refsToOriginalImplementations removeObjectForKey:keyValue]; +} + +static IMP +getRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; + return (IMP)[originalImplementationValue pointerValue]; +} + static void -swizzle(Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock) +swizzle( + Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock, const void *key) { Method method = class_getInstanceMethod(classToSwizzle, selector); @@ -106,6 +144,33 @@ @implementation SentrySwizzle pthread_mutex_lock(&gLock); originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); + if (originalIMP) { + storeRefToOriginalImplementation(key, originalIMP); + } + + pthread_mutex_unlock(&gLock); +} + +static void +unswizzle(Class classToUnswizzle, SEL selector, const void *key) +{ + Method method = class_getInstanceMethod(classToUnswizzle, selector); + + NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.", + NSStringFromSelector(selector), + class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle); + + static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; + + pthread_mutex_lock(&gLock); + + IMP originalIMP = getRefToOriginalImplementation(key); + if (originalIMP) { + const char *methodType = method_getTypeEncoding(method); + class_replaceMethod(classToUnswizzle, selector, originalIMP, methodType); + + removeRefToOriginalImplementation(key); + } pthread_mutex_unlock(&gLock); } @@ -164,7 +229,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector } } - swizzle(classToSwizzle, selector, factoryBlock); + swizzle(classToSwizzle, selector, factoryBlock, key); if (key) { [swizzledClassesForKey(key) addObject:classToSwizzle]; @@ -174,6 +239,29 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector return YES; } ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key +{ + NSAssert(key != NULL, @"Key may not be NULL."); + + if (key == NULL) { + NSLog(@"Key may not be NULL."); + return NO; + } + + @synchronized(swizzledClassesDictionary()) { + NSSet *swizzledClasses = swizzledClassesForKey(key); + if (![swizzledClasses containsObject:classToUnswizzle]) { + return NO; + } + + unswizzle(classToUnswizzle, selector, key); + + [swizzledClassesForKey(key) removeObject:classToUnswizzle]; + } + + return YES; +} + + (void)swizzleClassMethod:(SEL)selector inClass:(Class)classToSwizzle newImpFactory:(SentrySwizzleImpFactoryBlock)factoryBlock diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 1e21a23f6fe..995edb0699e 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -83,6 +83,18 @@ _SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \ SentrySwizzleMode, key) +/** + * Unswizzles the instance method of the class. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param selector Selector of the method that should be unswizzled. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ +#define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ + _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) + #pragma mark └ Swizzle Class Method /** @@ -302,6 +314,20 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:(SentrySwizzleMode)mode key:(const void *)key; +/** + * Unswizzles the instance method of the class. + * + * @param selector Selector of the method that should be unswizzled. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param key The key is used in combination with the mode to indicate whether the + * swizzling should be done for the given class. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key; + #pragma mark └ Swizzle Class method /** @@ -396,6 +422,9 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:SentrySwizzleMode \ key:KEY]; +#define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ + [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] + #define _SentrySwizzleClassMethod( \ classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \ [SentrySwizzle \ diff --git a/Sources/Sentry/include/SentryNSDataSwizzling.h b/Sources/Sentry/include/SentryNSDataSwizzling.h index 8b828f48806..8be8a97fc6b 100644 --- a/Sources/Sentry/include/SentryNSDataSwizzling.h +++ b/Sources/Sentry/include/SentryNSDataSwizzling.h @@ -13,6 +13,8 @@ SENTRY_NO_INIT - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker; +- (void)stop; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h index b9e3c1dad35..44076c34c5e 100644 --- a/Sources/Sentry/include/SentryNSFileManagerSwizzling.h +++ b/Sources/Sentry/include/SentryNSFileManagerSwizzling.h @@ -13,6 +13,8 @@ SENTRY_NO_INIT - (void)startWithOptions:(SentryOptions *)options tracker:(SentryFileIOTracker *)tracker; +- (void)stop; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index b489027cfe6..635e7be4642 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -83,6 +83,10 @@ - (void)methodForSwizzlingWithoutCallOriginal { }; +- (void)methodForUnswizzling +{ +}; + - (NSString *)string { return @"ABC"; @@ -353,4 +357,23 @@ - (void)testSwizzleDontCallOriginalImplementation XCTAssertThrows([a methodForSwizzlingWithoutCallOriginal]); } +- (void)testUnswizzleInstanceMethod +{ + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_B new]; + swizzleVoidMethod( + [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, + SentrySwizzleModeAlways, (void *)methodForUnswizzling); + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); + + [SentryTestsLog clear]; + + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + ASSERT_LOG_IS(@""); +} @end diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift index e1809633036..bffa66f46a8 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryFileIOTrackingIntegrationTests.swift @@ -51,6 +51,7 @@ class SentryFileIOTrackingIntegrationTests: XCTestCase { try? FileManager.default.removeItem(at: fixture.fileDirectory) } clearTestState() + SentrySDK.close() } func test_WritingTrackingDisabled_forIOOption() { diff --git a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m index d2ffc50af5d..7dc199e3eaf 100644 --- a/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m +++ b/Tests/SentryTests/Integrations/Performance/IO/SentryNSFileManagerSwizzlingTests.m @@ -73,6 +73,7 @@ - (void)tearDown [NSFileManager.defaultManager removeItemAtURL:fileDirectory error:nil]; } [self->tracker disable]; + [[SentryNSFileManagerSwizzling shared] stop]; } - (void)testNSFileManagerCreateFile_preiOS18macOS15tvOS18_experimentalFlagDisabled_shouldNotSwizzle From 77b42e097e23ca488a82ab78f9b9b55d3beaaa6d Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 19 Dec 2024 10:52:54 +0100 Subject: [PATCH 21/32] feat: add method unswizzling --- CHANGELOG.md | 1 + Sources/Sentry/SentrySwizzle.m | 92 ++++++++++++++++++- .../include/HybridPublic/SentrySwizzle.h | 29 ++++++ Tests/SentryTests/Helper/SentrySwizzleTests.m | 23 +++++ scripts/.clang-format-version | 2 +- 5 files changed, 144 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94661dc0431..4b95549b784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) +- Add method unswizzling ### Internal diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 13c37553557..6a37ee95ee9 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -40,8 +40,46 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +static NSMutableDictionary * +refsToOriginalImplementationsDictionary(void) +{ + static NSMutableDictionary *refsToOriginalImplementations; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ refsToOriginalImplementations = [NSMutableDictionary new]; }); + return refsToOriginalImplementations; +} + +static void +storeRefToOriginalImplementation(const void *key, IMP implementation) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation]; +} + +static void +removeRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + [refsToOriginalImplementations removeObjectForKey:keyValue]; +} + +static IMP +getRefToOriginalImplementation(const void *key) +{ + NSMutableDictionary *refsToOriginalImplementations + = refsToOriginalImplementationsDictionary(); + NSValue *keyValue = [NSValue valueWithPointer:key]; + NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; + return (IMP)[originalImplementationValue pointerValue]; +} + static void -swizzle(Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock) +swizzle( + Class classToSwizzle, SEL selector, SentrySwizzleImpFactoryBlock factoryBlock, const void *key) { Method method = class_getInstanceMethod(classToSwizzle, selector); @@ -106,6 +144,33 @@ @implementation SentrySwizzle pthread_mutex_lock(&gLock); originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); + if (originalIMP) { + storeRefToOriginalImplementation(key, originalIMP); + } + + pthread_mutex_unlock(&gLock); +} + +static void +unswizzle(Class classToUnswizzle, SEL selector, const void *key) +{ + Method method = class_getInstanceMethod(classToUnswizzle, selector); + + NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.", + NSStringFromSelector(selector), + class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle); + + static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; + + pthread_mutex_lock(&gLock); + + IMP originalIMP = getRefToOriginalImplementation(key); + if (originalIMP) { + const char *methodType = method_getTypeEncoding(method); + class_replaceMethod(classToUnswizzle, selector, originalIMP, methodType); + + removeRefToOriginalImplementation(key); + } pthread_mutex_unlock(&gLock); } @@ -164,7 +229,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector } } - swizzle(classToSwizzle, selector, factoryBlock); + swizzle(classToSwizzle, selector, factoryBlock, key); if (key) { [swizzledClassesForKey(key) addObject:classToSwizzle]; @@ -174,6 +239,29 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector return YES; } ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key +{ + NSAssert(key != NULL, @"Key may not be NULL."); + + if (key == NULL) { + NSLog(@"Key may not be NULL."); + return NO; + } + + @synchronized(swizzledClassesDictionary()) { + NSSet *swizzledClasses = swizzledClassesForKey(key); + if (![swizzledClasses containsObject:classToUnswizzle]) { + return NO; + } + + unswizzle(classToUnswizzle, selector, key); + + [swizzledClassesForKey(key) removeObject:classToUnswizzle]; + } + + return YES; +} + + (void)swizzleClassMethod:(SEL)selector inClass:(Class)classToSwizzle newImpFactory:(SentrySwizzleImpFactoryBlock)factoryBlock diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 1e21a23f6fe..995edb0699e 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -83,6 +83,18 @@ _SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \ SentrySwizzleMode, key) +/** + * Unswizzles the instance method of the class. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param selector Selector of the method that should be unswizzled. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ +#define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ + _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) + #pragma mark └ Swizzle Class Method /** @@ -302,6 +314,20 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:(SentrySwizzleMode)mode key:(const void *)key; +/** + * Unswizzles the instance method of the class. + * + * @param selector Selector of the method that should be unswizzled. + * + * @param classToUnswizzle The class with the method that should be unswizzled. + * + * @param key The key is used in combination with the mode to indicate whether the + * swizzling should be done for the given class. + * + * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. + */ ++ (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key; + #pragma mark └ Swizzle Class method /** @@ -396,6 +422,9 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:SentrySwizzleMode \ key:KEY]; +#define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ + [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] + #define _SentrySwizzleClassMethod( \ classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \ [SentrySwizzle \ diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index b489027cfe6..635e7be4642 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -83,6 +83,10 @@ - (void)methodForSwizzlingWithoutCallOriginal { }; +- (void)methodForUnswizzling +{ +}; + - (NSString *)string { return @"ABC"; @@ -353,4 +357,23 @@ - (void)testSwizzleDontCallOriginalImplementation XCTAssertThrows([a methodForSwizzlingWithoutCallOriginal]); } +- (void)testUnswizzleInstanceMethod +{ + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_B new]; + swizzleVoidMethod( + [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, + SentrySwizzleModeAlways, (void *)methodForUnswizzling); + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); + + [SentryTestsLog clear]; + + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + ASSERT_LOG_IS(@""); +} @end 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 60cb40f60c72554a0b15bcfb6bbede78260989c3 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Thu, 19 Dec 2024 10:59:03 +0100 Subject: [PATCH 22/32] add changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b95549b784..27849f95339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Add method unswizzling +- Add method unswizzling (#4647) ### Internal From bdc6d9556cc7f9d1dae72233df40d56e053a2d2e Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 20 Dec 2024 11:01:07 +0100 Subject: [PATCH 23/32] change unswizzling to be available in test environment only --- Sources/Sentry/SentrySwizzle.m | 9 ++++++++- .../Sentry/include/HybridPublic/SentrySwizzle.h | 15 +++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 6a37ee95ee9..c1d692da4c8 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -40,6 +40,7 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +#if TEST || TESTCI static NSMutableDictionary * refsToOriginalImplementationsDictionary(void) { @@ -76,6 +77,7 @@ @implementation SentrySwizzle NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; return (IMP)[originalImplementationValue pointerValue]; } +#endif // TEST || TESTCI static void swizzle( @@ -144,13 +146,16 @@ @implementation SentrySwizzle pthread_mutex_lock(&gLock); originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); +#if TEST || TESTCI if (originalIMP) { storeRefToOriginalImplementation(key, originalIMP); } +#endif // TEST || TESTCI pthread_mutex_unlock(&gLock); } +#if TEST || TESTCI static void unswizzle(Class classToUnswizzle, SEL selector, const void *key) { @@ -174,7 +179,7 @@ @implementation SentrySwizzle pthread_mutex_unlock(&gLock); } - +#endif // TEST || TESTCI static NSMutableDictionary *> * swizzledClassesDictionary(void) { @@ -239,6 +244,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector return YES; } +#if TEST || TESTCI + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key { NSAssert(key != NULL, @"Key may not be NULL."); @@ -261,6 +267,7 @@ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle ke return YES; } +#endif // TEST || TESTCI + (void)swizzleClassMethod:(SEL)selector inClass:(Class)classToSwizzle diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 995edb0699e..19fbf845eca 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -83,6 +83,7 @@ _SentrySWWrapArg(SentrySWArguments), _SentrySWWrapArg(SentrySWReplacement), \ SentrySwizzleMode, key) +#if TEST || TESTCI /** * Unswizzles the instance method of the class. * @@ -92,8 +93,10 @@ * * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ -#define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ - _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) +# define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ + _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) + +#endif // TEST || TESTCI #pragma mark └ Swizzle Class Method @@ -314,6 +317,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:(SentrySwizzleMode)mode key:(const void *)key; +#if TEST || TESTCI /** * Unswizzles the instance method of the class. * @@ -327,6 +331,7 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key; +#endif // TEST || TESTCI #pragma mark └ Swizzle Class method @@ -422,8 +427,10 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { mode:SentrySwizzleMode \ key:KEY]; -#define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ - [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] +#if TEST || TESTCI +# define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ + [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] +#endif // TEST || TESTCI #define _SentrySwizzleClassMethod( \ classToSwizzle, selector, SentrySWReturnType, SentrySWArguments, SentrySWReplacement) \ From 6ed2cfb080bb525b1659f4f4462614de828347f6 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Fri, 20 Dec 2024 11:10:09 +0100 Subject: [PATCH 24/32] add conditional compilation for unswizzling --- Sources/Sentry/SentryNSDataSwizzling.m | 2 ++ Sources/Sentry/SentryNSFileManagerSwizzling.m | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 79d1d182583..49d0bb750cf 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -117,6 +117,7 @@ + (void)swizzle + (void)unswizzle { +#if TEST || TESTCI SEL writeToFileAtomicallySelector = NSSelectorFromString(@"writeToFile:atomically:"); SentryUnswizzleInstanceMethod( NSData.class, writeToFileAtomicallySelector, (void *)writeToFileAtomicallySelector); @@ -138,6 +139,7 @@ + (void)unswizzle = NSSelectorFromString(@"initWithContentsOfURL:options:error:"); SentryUnswizzleInstanceMethod(NSData.class, initWithContentsOfURLOptionsErrorSelector, (void *)initWithContentsOfURLOptionsErrorSelector); +#endif // TEST || TESTCI } #pragma clang diagnostic pop @end diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index f00fe9e1e2d..78a5bd6996a 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -73,12 +73,14 @@ + (void)swizzle + (void)unswizzle { +#if TEST || TESTCI if (@available(iOS 18, macOS 15, tvOS 18, *)) { SEL createFileAtPathContentsAttributes = NSSelectorFromString(@"createFileAtPath:contents:attributes:"); SentryUnswizzleInstanceMethod(NSFileManager.class, createFileAtPathContentsAttributes, (void *)createFileAtPathContentsAttributes); } +#endif // TEST || TESTCI } #pragma clang diagnostic pop @end From 8cff52ef251124950df91d0d60f4b0faee5290d1 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 15:59:56 +0100 Subject: [PATCH 25/32] remove merge conflict in changelog --- CHANGELOG.md | 43 +------------------------------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa03da98e7..9a8d4387dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,44 +2,10 @@ ## Unreleased -### Internal - -- Update to Xcode 16.2 in workflows (#4673) - -## 8.43.0 - -> [!WARNING] -> This release contains a breaking change for the previously experimental session replay options. We moved the options from Session from `options.experimental.sessionReplay` to `options.sessionReplay`. - -### Features - -- Session replay GA (#4662) -- Show session replay options as replay tags (#4639) - -### Fixes - -- Remove empty session replay tags (#4667) -- - `SentrySdkInfo.packages` should be an array (#4626) -- Use the same SdkInfo for envelope header and event (#4629) - -### Improvements - -- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) -- Mask screenshots for errors (#4623) -- Slightly speed up serializing scope (#4661) - -### Internal - -- Remove loading `integrations` names from `event.extra` (#4627) -- Add Hybrid SDKs API to add extra SDK packages (#4637) - -## 8.43.0-beta.1 - ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) - Mask screenshots for errors (#4623) -- Slightly speed up serializing scope (#4661) ### Features @@ -49,22 +15,15 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Add method unswizzling (#4647) - Fixes Session replay screenshot provider crash (#4649) - Session Replay wrong clipping order (#4651) +- Add method unswizzling (#4647) ### Internal - Remove loading `integrations` names from `event.extra` (#4627) - Add Hybrid SDKs API to add extra SDK packages (#4637) -## 8.42.1 - -### Fixes - -- Fixes Session replay screenshot provider crash (#4649) -- Session Replay wrong clipping order (#4651) - ## 8.42.0 ### Features From 649105ee63502c8f6405233a9d08862884a69d99 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:37:49 +0100 Subject: [PATCH 26/32] add more documentation, null handling and test cases --- Sources/Sentry/SentrySwizzle.m | 72 +++++++++++++++++-- .../include/HybridPublic/SentrySwizzle.h | 5 ++ Tests/SentryTests/Helper/SentrySwizzleTests.m | 63 +++++++++++++++- 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index c1d692da4c8..c1ee3bf47e6 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -1,4 +1,5 @@ #import "SentrySwizzle.h" +#import "SentryLog.h" #import #include @@ -40,7 +41,14 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation @implementation SentrySwizzle +// This lock is shared by all swizzling and unswizzling calls to ensure that +// only one thread is modifying the class at a time. +static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; + #if TEST || TESTCI +/** + * - Returns: a dictionary that maps keys to the references to the original implementations. + */ static NSMutableDictionary * refsToOriginalImplementationsDictionary(void) { @@ -50,31 +58,73 @@ @implementation SentrySwizzle return refsToOriginalImplementations; } +/** + * Adds a reference to the original implementation to the dictionary. + * + * If the key is NULL, it will log an error and NOT store the reference. + * + * - Parameter key: The key for which to store the reference to the original implementation. + * - Parameter implementation: Reference to the original implementation to store. + */ static void storeRefToOriginalImplementation(const void *key, IMP implementation) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; refsToOriginalImplementations[keyValue] = [NSValue valueWithPointer:implementation]; } +/** + * Removes a reference to the original implementation from the dictionary. + * + * If the key is NULL, it will log an error and do nothing. + * + * - Parameter key: The key for which to remove the reference to the original implementation. + */ static void removeRefToOriginalImplementation(const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; [refsToOriginalImplementations removeObjectForKey:keyValue]; } +/** + * Returns the original implementation for the given key. + * + * If the key is NULL, it will log an error and return NULL. + * If no original implementation is found, it will return NULL. + * + * - Parameter key: The key for which to get the original implementation. + * - Returns: The original implementation for the given key. + */ static IMP getRefToOriginalImplementation(const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key == NULL) { + SENTRY_LOG_ERROR(@"Key may not be NULL."); + return NULL; + } NSMutableDictionary *refsToOriginalImplementations = refsToOriginalImplementationsDictionary(); NSValue *keyValue = [NSValue valueWithPointer:key]; NSValue *originalImplementationValue = [refsToOriginalImplementations objectForKey:keyValue]; + if (originalImplementationValue == nil) { + return NULL; + } return (IMP)[originalImplementationValue pointerValue]; } #endif // TEST || TESTCI @@ -89,8 +139,6 @@ @implementation SentrySwizzle NSStringFromSelector(selector), class_isMetaClass(classToSwizzle) ? @"class" : @"instance", classToSwizzle); - static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; - // To keep things thread-safe, we fill in the originalIMP later, // with the result of the class_replaceMethod call below. __block IMP originalIMP = NULL; @@ -148,7 +196,12 @@ @implementation SentrySwizzle originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); #if TEST || TESTCI if (originalIMP) { - storeRefToOriginalImplementation(key, originalIMP); + NSCAssert(key != NULL, @"Key may not be NULL."); + if (key != NULL) { + storeRefToOriginalImplementation(key, originalIMP); + } else { + SENTRY_LOG_WARN(@"Key may not be NULL."); + } } #endif // TEST || TESTCI @@ -159,14 +212,19 @@ @implementation SentrySwizzle static void unswizzle(Class classToUnswizzle, SEL selector, const void *key) { + NSCAssert(key != NULL, @"Key may not be NULL."); + + if (key == NULL) { + SENTRY_LOG_WARN(@"Key may not be NULL."); + return; + } + Method method = class_getInstanceMethod(classToUnswizzle, selector); NSCAssert(NULL != method, @"Selector %@ not found in %@ methods of class %@.", NSStringFromSelector(selector), class_isMetaClass(classToUnswizzle) ? @"class" : @"instance", classToUnswizzle); - static pthread_mutex_t gLock = PTHREAD_MUTEX_INITIALIZER; - pthread_mutex_lock(&gLock); IMP originalIMP = getRefToOriginalImplementation(key); @@ -213,7 +271,7 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector @"Key may not be NULL if mode is not SentrySwizzleModeAlways."); if (key == NULL && mode != SentrySwizzleModeAlways) { - NSLog(@"Key may not be NULL if mode is not SentrySwizzleModeAlways."); + SENTRY_LOG_WARN(@"Key may not be NULL if mode is not SentrySwizzleModeAlways."); return NO; } @@ -250,7 +308,7 @@ + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle ke NSAssert(key != NULL, @"Key may not be NULL."); if (key == NULL) { - NSLog(@"Key may not be NULL."); + SENTRY_LOG_WARN(@"Key may not be NULL."); return NO; } diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 19fbf845eca..0493a96ba50 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -86,6 +86,8 @@ #if TEST || TESTCI /** * Unswizzles the instance method of the class. + * To reduce the risk of breaking functionality with unswizzling, this method is not considered + * safe-to-use in production and only available in test targets. * * @param classToUnswizzle The class with the method that should be unswizzled. * @@ -321,6 +323,9 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { /** * Unswizzles the instance method of the class. * + * To reduce the risk of breaking functionality with unswizzling, this method is not considered + * safe-to-use in production and only available in test targets. + * * @param selector Selector of the method that should be unswizzled. * * @param classToUnswizzle The class with the method that should be unswizzled. diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index 635e7be4642..882ab2dbd74 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -359,21 +359,82 @@ - (void)testSwizzleDontCallOriginalImplementation - (void)testUnswizzleInstanceMethod { + // -- Arrange -- SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); - SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_B new]; + + // Swizzle the method once swizzleVoidMethod( [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, SentrySwizzleModeAlways, (void *)methodForUnswizzling); + + // Smoke test the swizzling [object methodForUnswizzling]; ASSERT_LOG_IS(@"A"); + [SentryTestsLog clear]; + + // -- Act -- + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + + // -- Assert -- + ASSERT_LOG_IS(@""); +} +- (void)testUnswizzleInstanceMethod_methodNotSwizzled_shouldWork +{ + // -- Arrange -- + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_A new]; + + // Smoke-test the swizzling + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); [SentryTestsLog clear]; + // -- Act -- [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling inClass:[SentrySwizzleTestClass_A class] key:(void *)methodForUnswizzling]; [object methodForUnswizzling]; + + // -- Assert -- ASSERT_LOG_IS(@""); } + +- (void)testUnswizzleInstanceMethod_unswizzlingMethodMultipleTimes_shouldWork +{ + // -- Arrange -- + SEL methodForUnswizzling = NSSelectorFromString(@"methodForUnswizzling"); + SentrySwizzleTestClass_A *object = [SentrySwizzleTestClass_A new]; + + swizzleVoidMethod( + [SentrySwizzleTestClass_A class], methodForUnswizzling, ^{ SentryTestsLog(@"A"); }, + SentrySwizzleModeAlways, (void *)methodForUnswizzling); + + // Smoke test the swizzling + [object methodForUnswizzling]; + ASSERT_LOG_IS(@"A"); + [SentryTestsLog clear]; + + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + ASSERT_LOG_IS(@""); + [SentryTestsLog clear]; + + // -- Act -- + // Unswizzle again should not cause issues + [SentrySwizzle unswizzleInstanceMethod:methodForUnswizzling + inClass:[SentrySwizzleTestClass_A class] + key:(void *)methodForUnswizzling]; + [object methodForUnswizzling]; + + // -- Assert - + ASSERT_LOG_IS(@""); +} + @end From 55231ef0c04008bb60dc38747e77a619bfd60f4e Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:43:04 +0100 Subject: [PATCH 27/32] replace NSLog with SentryLog --- Sources/Sentry/SentrySwizzle.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index c1ee3bf47e6..93895568972 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -21,7 +21,7 @@ - (SentrySwizzleOriginalIMP)getOriginalImplementation { NSAssert(_impProviderBlock, @"_impProviderBlock can't be missing"); if (!_impProviderBlock) { - NSLog(@"_impProviderBlock can't be missing"); + SENTRY_LOG_ERROR(@"_impProviderBlock can't be missing"); return NULL; } From 451caaee9cebee2c67ca6ecb7132bc5ea875658a Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:49:56 +0100 Subject: [PATCH 28/32] add workflow dispatch for testflight upload --- .github/workflows/testflight.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index fbc46498492..b17283b8978 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - '.github/workflows/testflight.yml' + workflow_dispatch: jobs: upload_to_testflight: From 4bc7b322065aa9a0fefa869b8b85621d0a1a3320 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Tue, 7 Jan 2025 16:51:12 +0100 Subject: [PATCH 29/32] remove workflow dispatch --- .github/workflows/testflight.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/testflight.yml b/.github/workflows/testflight.yml index b17283b8978..fbc46498492 100644 --- a/.github/workflows/testflight.yml +++ b/.github/workflows/testflight.yml @@ -14,7 +14,6 @@ on: pull_request: paths: - '.github/workflows/testflight.yml' - workflow_dispatch: jobs: upload_to_testflight: From 4dc0029a6f5a7bc5d308c4b1afab9b39f2d02f65 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 8 Jan 2025 11:47:41 +0100 Subject: [PATCH 30/32] fix issues and merge conflicts --- CHANGELOG.md | 45 +++++++++++++++++-- Sources/Sentry/SentrySwizzle.m | 7 ++- .../include/HybridPublic/SentrySwizzle.h | 25 +++++++---- Tests/SentryTests/Helper/SentrySwizzleTests.m | 2 +- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8d4387dfd..588e150a99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,45 @@ ## Unreleased +### Internal + +- Update to Xcode 16.2 in workflows (#4673) +- Add method unswizzling (#4647) + +## 8.43.0 + +> [!WARNING] +> This release contains a breaking change for the previously experimental session replay options. We moved the options from Session from `options.experimental.sessionReplay` to `options.sessionReplay`. + +### Features + +- Session replay GA (#4662) +- Show session replay options as replay tags (#4639) + +### Fixes + +- Remove empty session replay tags (#4667) +- - `SentrySdkInfo.packages` should be an array (#4626) +- Use the same SdkInfo for envelope header and event (#4629) + +### Improvements + +- Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) +- Mask screenshots for errors (#4623) +- Slightly speed up serializing scope (#4661) + +### Internal + +- Remove loading `integrations` names from `event.extra` (#4627) +- Add Hybrid SDKs API to add extra SDK packages (#4637) + +## 8.43.0-beta.1 + ### Improvements - Improve compiler error message for missing Swift declarations due to APPLICATION_EXTENSION_API_ONLY (#4603) - Mask screenshots for errors (#4623) +- Slightly speed up serializing scope (#4661) ### Features @@ -15,15 +50,19 @@ - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) -- Fixes Session replay screenshot provider crash (#4649) -- Session Replay wrong clipping order (#4651) -- Add method unswizzling (#4647) ### Internal - Remove loading `integrations` names from `event.extra` (#4627) - Add Hybrid SDKs API to add extra SDK packages (#4637) +## 8.42.1 + +### Fixes + +- Fixes Session replay screenshot provider crash (#4649) +- Session Replay wrong clipping order (#4651) + ## 8.42.0 ### Features diff --git a/Sources/Sentry/SentrySwizzle.m b/Sources/Sentry/SentrySwizzle.m index 93895568972..c8f10fb8e95 100644 --- a/Sources/Sentry/SentrySwizzle.m +++ b/Sources/Sentry/SentrySwizzle.m @@ -196,11 +196,11 @@ @implementation SentrySwizzle originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); #if TEST || TESTCI if (originalIMP) { - NSCAssert(key != NULL, @"Key may not be NULL."); if (key != NULL) { storeRefToOriginalImplementation(key, originalIMP); } else { - SENTRY_LOG_WARN(@"Key may not be NULL."); + SENTRY_LOG_WARN( + @"Swizzling without a key is not recommended, because they can not be unswizzled."); } } #endif // TEST || TESTCI @@ -213,7 +213,6 @@ @implementation SentrySwizzle unswizzle(Class classToUnswizzle, SEL selector, const void *key) { NSCAssert(key != NULL, @"Key may not be NULL."); - if (key == NULL) { SENTRY_LOG_WARN(@"Key may not be NULL."); return; @@ -238,6 +237,7 @@ @implementation SentrySwizzle pthread_mutex_unlock(&gLock); } #endif // TEST || TESTCI + static NSMutableDictionary *> * swizzledClassesDictionary(void) { @@ -306,7 +306,6 @@ + (BOOL)swizzleInstanceMethod:(SEL)selector + (BOOL)unswizzleInstanceMethod:(SEL)selector inClass:(Class)classToUnswizzle key:(const void *)key { NSAssert(key != NULL, @"Key may not be NULL."); - if (key == NULL) { SENTRY_LOG_WARN(@"Key may not be NULL."); return NO; diff --git a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h index 0493a96ba50..3b789d1b5a7 100644 --- a/Sources/Sentry/include/HybridPublic/SentrySwizzle.h +++ b/Sources/Sentry/include/HybridPublic/SentrySwizzle.h @@ -86,18 +86,18 @@ #if TEST || TESTCI /** * Unswizzles the instance method of the class. - * To reduce the risk of breaking functionality with unswizzling, this method is not considered - * safe-to-use in production and only available in test targets. * - * @param classToUnswizzle The class with the method that should be unswizzled. + * @warning To reduce the risk of breaking functionality with unswizzling, this method is not + * considered safe-to-use in production and only available in test targets. * + * @param classToUnswizzle The class with the method that should be unswizzled. * @param selector Selector of the method that should be unswizzled. + * @param key The key to unswizzle the method with. * * @return @c YES if successfully unswizzled and @c NO if the method was not swizzled. */ # define SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) \ _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, key) - #endif // TEST || TESTCI #pragma mark └ Swizzle Class Method @@ -323,13 +323,11 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { /** * Unswizzles the instance method of the class. * - * To reduce the risk of breaking functionality with unswizzling, this method is not considered - * safe-to-use in production and only available in test targets. + * @warning To reduce the risk of breaking functionality with unswizzling, this method is not + * considered safe-to-use in production and only available in test targets. * * @param selector Selector of the method that should be unswizzled. - * * @param classToUnswizzle The class with the method that should be unswizzled. - * * @param key The key is used in combination with the mode to indicate whether the * swizzling should be done for the given class. * @@ -433,6 +431,17 @@ typedef NS_ENUM(NSUInteger, SentrySwizzleMode) { key:KEY]; #if TEST || TESTCI +/** + * Macro to unswizzle an instance method. + * + * @warning To reduce the risk of breaking functionality with unswizzling, this macro is not + * considered safe-to-use in production and only available in test targets. + * + * @param classToUnswizzle The class to unswizzle the method from. + * @param selector The selector of the method to unswizzle. + * @param KEY The key to unswizzle the method with. + * @return @c YES if the method was successfully unswizzled, @c NO otherwise. + */ # define _SentryUnswizzleInstanceMethod(classToUnswizzle, selector, KEY) \ [SentrySwizzle unswizzleInstanceMethod:selector inClass:[classToUnswizzle class] key:KEY] #endif // TEST || TESTCI diff --git a/Tests/SentryTests/Helper/SentrySwizzleTests.m b/Tests/SentryTests/Helper/SentrySwizzleTests.m index 882ab2dbd74..ec30b5f44f9 100644 --- a/Tests/SentryTests/Helper/SentrySwizzleTests.m +++ b/Tests/SentryTests/Helper/SentrySwizzleTests.m @@ -391,7 +391,7 @@ - (void)testUnswizzleInstanceMethod_methodNotSwizzled_shouldWork // Smoke-test the swizzling [object methodForUnswizzling]; - ASSERT_LOG_IS(@"A"); + ASSERT_LOG_IS(@""); [SentryTestsLog clear]; // -- Act -- From 717ae41e1ac06a923a29baab74b2bd770b529e76 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 8 Jan 2025 12:16:41 +0100 Subject: [PATCH 31/32] fix changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace1f87d4c8..ef25d4789ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixes - Replace occurences of `strncpy` with `strlcpy` (#4636) +- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18. This feature is experimental and must be enabled by setting the option `experimental.enableFileManagerSwizzling` to `true` (#4634) + ### Internal @@ -52,7 +54,6 @@ ### Fixes -- Fix span recording for `NSFileManager.createFileAtPath` starting with iOS 18, macOS 15 and tvOS 18. This feature is experimental and must be enabled by setting the option `experimental.enableFileManagerSwizzling` to `true` (#4634) - `SentrySdkInfo.packages` should be an array (#4626) - Use the same SdkInfo for envelope header and event (#4629) From 5827fd2de516debbd54790b4581a4763c2c63fa8 Mon Sep 17 00:00:00 2001 From: Philip Niedertscheider Date: Wed, 8 Jan 2025 13:30:42 +0100 Subject: [PATCH 32/32] add more comments --- Sources/Sentry/SentryNSDataSwizzling.m | 1 + Sources/Sentry/SentryNSFileManagerSwizzling.m | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/Sentry/SentryNSDataSwizzling.m b/Sources/Sentry/SentryNSDataSwizzling.m index 49d0bb750cf..6ddd52471a1 100644 --- a/Sources/Sentry/SentryNSDataSwizzling.m +++ b/Sources/Sentry/SentryNSDataSwizzling.m @@ -118,6 +118,7 @@ + (void)swizzle + (void)unswizzle { #if TEST || TESTCI + // Unswizzling is only supported in test targets as it is considered unsafe for production. SEL writeToFileAtomicallySelector = NSSelectorFromString(@"writeToFile:atomically:"); SentryUnswizzleInstanceMethod( NSData.class, writeToFileAtomicallySelector, (void *)writeToFileAtomicallySelector); diff --git a/Sources/Sentry/SentryNSFileManagerSwizzling.m b/Sources/Sentry/SentryNSFileManagerSwizzling.m index 78a5bd6996a..a55f4883837 100644 --- a/Sources/Sentry/SentryNSFileManagerSwizzling.m +++ b/Sources/Sentry/SentryNSFileManagerSwizzling.m @@ -74,6 +74,7 @@ + (void)swizzle + (void)unswizzle { #if TEST || TESTCI + // Unswizzling is only supported in test targets as it is considered unsafe for production. if (@available(iOS 18, macOS 15, tvOS 18, *)) { SEL createFileAtPathContentsAttributes = NSSelectorFromString(@"createFileAtPath:contents:attributes:");