From f438610a4dd37ad20f480977d592b4d09fc6bca2 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 3 Apr 2024 12:56:18 +0200 Subject: [PATCH] feat: Add the option swizzleClassNameExcludes (#3813) Add an option to exclude certain classes from swizzling. Fixes GH-3798 --- CHANGELOG.md | 1 + Sources/Sentry/Public/SentryOptions.h | 15 +++++++++++++++ Sources/Sentry/SentryOptions.m | 6 ++++++ .../SentryPerformanceTrackingIntegration.m | 5 +++-- Sources/Sentry/SentrySubClassFinder.m | 19 +++++++++++++++++++ Sources/Sentry/include/SentrySubClassFinder.h | 3 ++- .../SentrySubClassFinderTests.swift | 19 ++++++++++++------- ...SentryUIViewControllerSwizzlingTests.swift | 2 +- Tests/SentryTests/SentryOptionsTest.m | 13 +++++++++++++ 9 files changed, 72 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e905e20fd3..15614d95e09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ more about how to use the Metrics API. - Pre-main profiling data is now attached to the app start transaction (#3736) - Release framework without UIKit/AppKit (#3793) +- Add the option swizzleClassNameExcludes (#3813) ### Fixes diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 963a436bfc0..f079aa4bd44 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -368,6 +368,21 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enableSwizzling; +/** + * An array of class names to ignore for swizzling. + * + * @discussion The SDK checks if a class name of a class to swizzle contains a class name of this + * array. For example, if you add MyUIViewController to this list, the SDK excludes the following + * classes from swizzling: YourApp.MyUIViewController, YourApp.MyUIViewControllerA, + * MyApp.MyUIViewController. + * We can't use an @c NSArray here because we use this as a workaround for which users have + * to pass in class names that aren't available on specific iOS versions. By using @c + * NSArray, users can specify unavailable class names. + * + * @note Default is an empty array. + */ +@property (nonatomic, strong) NSSet *swizzleClassNameExcludes; + /** * When enabled, the SDK tracks the performance of Core Data operations. It requires enabling * performance monitoring. The default is @c YES. diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 5b6020f2913..635eb27f4b8 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -128,6 +128,7 @@ - (instancetype)init #endif // SENTRY_TARGET_PROFILING_SUPPORTED self.enableCoreDataTracing = YES; _enableSwizzling = YES; + self.swizzleClassNameExcludes = [NSSet new]; self.sendClientReports = YES; self.swiftAsyncStacktraces = NO; self.enableSpotlight = NO; @@ -449,6 +450,11 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableSwizzling"] block:^(BOOL value) { self->_enableSwizzling = value; }]; + if ([options[@"swizzleClassNameExcludes"] isKindOfClass:[NSSet class]]) { + _swizzleClassNameExcludes = + [options[@"swizzleClassNameExcludes"] filteredSetUsingPredicate:isNSString]; + } + [self setBool:options[@"enableCoreDataTracing"] block:^(BOOL value) { self->_enableCoreDataTracing = value; }]; diff --git a/Sources/Sentry/SentryPerformanceTrackingIntegration.m b/Sources/Sentry/SentryPerformanceTrackingIntegration.m index 8b3e5c16d3d..c3bfc6517be 100644 --- a/Sources/Sentry/SentryPerformanceTrackingIntegration.m +++ b/Sources/Sentry/SentryPerformanceTrackingIntegration.m @@ -34,8 +34,9 @@ - (BOOL)installWithOptions:(SentryOptions *)options attributes:attributes]; SentrySubClassFinder *subClassFinder = [[SentrySubClassFinder alloc] - initWithDispatchQueue:dispatchQueue - objcRuntimeWrapper:[SentryDefaultObjCRuntimeWrapper sharedInstance]]; + initWithDispatchQueue:dispatchQueue + objcRuntimeWrapper:[SentryDefaultObjCRuntimeWrapper sharedInstance] + swizzleClassNameExcludes:options.swizzleClassNameExcludes]; self.swizzling = [[SentryUIViewControllerSwizzling alloc] initWithOptions:options diff --git a/Sources/Sentry/SentrySubClassFinder.m b/Sources/Sentry/SentrySubClassFinder.m index d01ba940e1c..6dbb5a15f99 100644 --- a/Sources/Sentry/SentrySubClassFinder.m +++ b/Sources/Sentry/SentrySubClassFinder.m @@ -14,6 +14,7 @@ @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @property (nonatomic, strong) id objcRuntimeWrapper; +@property (nonatomic, copy) NSSet *swizzleClassNameExcludes; @end @@ -21,10 +22,12 @@ @implementation SentrySubClassFinder - (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue objcRuntimeWrapper:(id)objcRuntimeWrapper + swizzleClassNameExcludes:(NSSet *)swizzleClassNameExcludes { if (self = [super init]) { self.dispatchQueue = dispatchQueue; self.objcRuntimeWrapper = objcRuntimeWrapper; + self.swizzleClassNameExcludes = swizzleClassNameExcludes; } return self; } @@ -58,6 +61,22 @@ - (void)actOnSubclassesOfViewControllerInImage:(NSString *)imageName block:(void NSMutableArray *classesToSwizzle = [NSMutableArray new]; for (int i = 0; i < count; i++) { NSString *className = [NSString stringWithUTF8String:classes[i]]; + + BOOL shouldExcludeClassFromSwizzling = NO; + for (NSString *swizzleClassNameExclude in self.swizzleClassNameExcludes) { + if ([className containsString:swizzleClassNameExclude]) { + shouldExcludeClassFromSwizzling = YES; + break; + } + } + + // It is vital to avoid calling NSClassFromString for the excluded classes because we + // had crashes for specific classes when calling NSClassFromString, such as + // https://github.com/getsentry/sentry-cocoa/issues/3798. + if (shouldExcludeClassFromSwizzling) { + continue; + } + Class class = NSClassFromString(className); if ([self isClass:class subClassOf:viewControllerClass]) { [classesToSwizzle addObject:className]; diff --git a/Sources/Sentry/include/SentrySubClassFinder.h b/Sources/Sentry/include/SentrySubClassFinder.h index 3c7a26ade80..842d1478f46 100644 --- a/Sources/Sentry/include/SentrySubClassFinder.h +++ b/Sources/Sentry/include/SentrySubClassFinder.h @@ -10,7 +10,8 @@ NS_ASSUME_NONNULL_BEGIN SENTRY_NO_INIT - (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue - objcRuntimeWrapper:(id)objcRuntimeWrapper; + objcRuntimeWrapper:(id)objcRuntimeWrapper + swizzleClassNameExcludes:(NSSet *)swizzleClassNameExcludes; #if SENTRY_HAS_UIKIT /** diff --git a/Tests/SentryTests/Integrations/Performance/SentrySubClassFinderTests.swift b/Tests/SentryTests/Integrations/Performance/SentrySubClassFinderTests.swift index 85dfe85aefd..af7c3087cd4 100644 --- a/Tests/SentryTests/Integrations/Performance/SentrySubClassFinderTests.swift +++ b/Tests/SentryTests/Integrations/Performance/SentrySubClassFinderTests.swift @@ -27,8 +27,8 @@ class SentrySubClassFinderTests: XCTestCase { } } - var sut: SentrySubClassFinder { - return SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: runtimeWrapper) + func getSut(swizzleClassNameExcludes: Set = []) -> SentrySubClassFinder { + return SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: runtimeWrapper, swizzleClassNameExcludes: swizzleClassNameExcludes) } } @@ -43,6 +43,10 @@ class SentrySubClassFinderTests: XCTestCase { assertActOnSubclassesOfViewController(expected: [FirstViewController.self, SecondViewController.self, ViewControllerNumberThree.self, VCAnyNaming.self]) } + func testActOnSubclassesOfViewController_WithSwizzleClassNameExcludes() { + assertActOnSubclassesOfViewController(expected: [SecondViewController.self, ViewControllerNumberThree.self], swizzleClassNameExcludes: ["FirstViewController", "VCAnyNaming"]) + } + func testActOnSubclassesOfViewController_NoViewController() { fixture.runtimeWrapper.classesNames = { _ in [] } assertActOnSubclassesOfViewController(expected: []) @@ -59,7 +63,7 @@ class SentrySubClassFinderTests: XCTestCase { } func testGettingSubclasses_DoesNotCallInitializer() { - let sut = SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: fixture.runtimeWrapper) + let sut = SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: fixture.runtimeWrapper, swizzleClassNameExcludes: []) var actual: [AnyClass] = [] sut.actOnSubclassesOfViewController(inImage: fixture.imageName) { subClass in @@ -69,11 +73,11 @@ class SentrySubClassFinderTests: XCTestCase { XCTAssertFalse(SentryInitializeForGettingSubclassesCalled.wasCalled()) } - private func assertActOnSubclassesOfViewController(expected: [AnyClass]) { - assertActOnSubclassesOfViewController(expected: expected, imageName: fixture.imageName) + private func assertActOnSubclassesOfViewController(expected: [AnyClass], swizzleClassNameExcludes: Set = []) { + assertActOnSubclassesOfViewController(expected: expected, imageName: fixture.imageName, swizzleClassNameExcludes: swizzleClassNameExcludes) } - private func assertActOnSubclassesOfViewController(expected: [AnyClass], imageName: String) { + private func assertActOnSubclassesOfViewController(expected: [AnyClass], imageName: String, swizzleClassNameExcludes: Set = []) { let expect = expectation(description: "") if expected.isEmpty { @@ -83,7 +87,8 @@ class SentrySubClassFinderTests: XCTestCase { } var actual: [AnyClass] = [] - fixture.sut.actOnSubclassesOfViewController(inImage: imageName) { subClass in + let sut = fixture.getSut(swizzleClassNameExcludes: swizzleClassNameExcludes) + sut.actOnSubclassesOfViewController(inImage: imageName) { subClass in XCTAssertTrue(Thread.isMainThread, "Block must be executed on the main thread.") actual.append(subClass) expect.fulfill() diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift index 4107efb5e05..88079affad2 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift @@ -15,7 +15,7 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { let binaryImageCache: SentryBinaryImageCache init() { - subClassFinder = TestSubClassFinder(dispatchQueue: dispatchQueue, objcRuntimeWrapper: objcRuntimeWrapper) + subClassFinder = TestSubClassFinder(dispatchQueue: dispatchQueue, objcRuntimeWrapper: objcRuntimeWrapper, swizzleClassNameExcludes: []) binaryImageCache = SentryDependencyContainer.sharedInstance().binaryImageCache } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 4df2b2cb684..98e57364ade 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -555,6 +555,7 @@ - (void)testNSNull_SetsDefaultValue @"inAppExcludes" : [NSNull null], @"urlSessionDelegate" : [NSNull null], @"enableSwizzling" : [NSNull null], + @"swizzleClassNameExcludes" : [NSNull null], @"enableIOTracking" : [NSNull null], @"sdk" : [NSNull null], @"enableCaptureFailedRequests" : [NSNull null], @@ -614,6 +615,7 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(@[], options.inAppExcludes); XCTAssertNil(options.urlSessionDelegate); XCTAssertEqual(YES, options.enableSwizzling); + XCTAssertEqual([NSSet new], options.swizzleClassNameExcludes); XCTAssertEqual(YES, options.enableFileIOTracing); XCTAssertEqual(YES, options.enableAutoBreadcrumbTracking); XCTAssertFalse(options.swiftAsyncStacktraces); @@ -811,6 +813,17 @@ - (void)testEnableSwizzling [self testBooleanField:@"enableSwizzling"]; } +- (void)testSwizzleClassNameExcludes +{ + NSSet *expected = [NSSet setWithObjects:@"Sentry", nil]; + NSSet *swizzleClassNameExcludes = [NSSet setWithObjects:@"Sentry", @2, nil]; + + SentryOptions *options = + [self getValidOptions:@{ @"swizzleClassNameExcludes" : swizzleClassNameExcludes }]; + + XCTAssertEqualObjects(expected, options.swizzleClassNameExcludes); +} + - (void)testEnableTracing { SentryOptions *options = [self getValidOptions:@{ @"enableTracing" : @YES }];