Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add the option swizzleClassNameExcludes #3813

Merged
merged 4 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading