Skip to content

Commit

Permalink
feat: Add the option swizzleClassNameExcludes (#3813)
Browse files Browse the repository at this point in the history
Add an option to exclude certain classes from swizzling.

Fixes GH-3798
  • Loading branch information
philipphofmann committed Apr 3, 2024
1 parent 1ee3d54 commit f438610
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<Class> 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<NSString *>, users can specify unavailable class names.
*
* @note Default is an empty array.
*/
@property (nonatomic, strong) NSSet<NSString *> *swizzleClassNameExcludes;

/**
* When enabled, the SDK tracks the performance of Core Data operations. It requires enabling
* performance monitoring. The default is @c YES.
Expand Down
6 changes: 6 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -449,6 +450,11 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)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; }];

Expand Down
5 changes: 3 additions & 2 deletions Sources/Sentry/SentryPerformanceTrackingIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions Sources/Sentry/SentrySubClassFinder.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@

@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue;
@property (nonatomic, strong) id<SentryObjCRuntimeWrapper> objcRuntimeWrapper;
@property (nonatomic, copy) NSSet<NSString *> *swizzleClassNameExcludes;

@end

@implementation SentrySubClassFinder

- (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue
objcRuntimeWrapper:(id<SentryObjCRuntimeWrapper>)objcRuntimeWrapper
swizzleClassNameExcludes:(NSSet<NSString *> *)swizzleClassNameExcludes
{
if (self = [super init]) {
self.dispatchQueue = dispatchQueue;
self.objcRuntimeWrapper = objcRuntimeWrapper;
self.swizzleClassNameExcludes = swizzleClassNameExcludes;
}
return self;
}
Expand Down Expand Up @@ -58,6 +61,22 @@ - (void)actOnSubclassesOfViewControllerInImage:(NSString *)imageName block:(void
NSMutableArray<NSString *> *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];
Expand Down
3 changes: 2 additions & 1 deletion Sources/Sentry/include/SentrySubClassFinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ NS_ASSUME_NONNULL_BEGIN
SENTRY_NO_INIT

- (instancetype)initWithDispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue
objcRuntimeWrapper:(id<SentryObjCRuntimeWrapper>)objcRuntimeWrapper;
objcRuntimeWrapper:(id<SentryObjCRuntimeWrapper>)objcRuntimeWrapper
swizzleClassNameExcludes:(NSSet<NSString *> *)swizzleClassNameExcludes;

#if SENTRY_HAS_UIKIT
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ class SentrySubClassFinderTests: XCTestCase {
}
}

var sut: SentrySubClassFinder {
return SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: runtimeWrapper)
func getSut(swizzleClassNameExcludes: Set<String> = []) -> SentrySubClassFinder {
return SentrySubClassFinder(dispatchQueue: TestSentryDispatchQueueWrapper(), objcRuntimeWrapper: runtimeWrapper, swizzleClassNameExcludes: swizzleClassNameExcludes)
}
}

Expand All @@ -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: [])
Expand All @@ -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
Expand All @@ -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<String> = []) {
assertActOnSubclassesOfViewController(expected: expected, imageName: fixture.imageName, swizzleClassNameExcludes: swizzleClassNameExcludes)
}

private func assertActOnSubclassesOfViewController(expected: [AnyClass], imageName: String) {
private func assertActOnSubclassesOfViewController(expected: [AnyClass], imageName: String, swizzleClassNameExcludes: Set<String> = []) {
let expect = expectation(description: "")

if expected.isEmpty {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
13 changes: 13 additions & 0 deletions Tests/SentryTests/SentryOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -811,6 +813,17 @@ - (void)testEnableSwizzling
[self testBooleanField:@"enableSwizzling"];
}

- (void)testSwizzleClassNameExcludes
{
NSSet<NSString *> *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 }];
Expand Down

0 comments on commit f438610

Please sign in to comment.