diff --git a/CHANGELOG.md b/CHANGELOG.md index b8591d8e2e..1ea4d15006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - Keep PropagationContext when cloning scope (#4518) +- UIViewController with Xcode 16 in debug (#4523). The Xcode 16 build setting [ENABLE_DEBUG_DYLIB](https://developer.apple.com/documentation/xcode/build-settings-reference#Enable-Debug-Dylib-Support), which is turned on by default only in debug, could lead to missing UIViewController traces. - Concurrency crash with Swift 6 (#4512) ## 8.40.1 diff --git a/Sources/Sentry/SentryBinaryImageCache.m b/Sources/Sentry/SentryBinaryImageCache.m index 0163aa18bb..fa030edf8b 100644 --- a/Sources/Sentry/SentryBinaryImageCache.m +++ b/Sources/Sentry/SentryBinaryImageCache.m @@ -141,16 +141,18 @@ - (NSInteger)indexOfImage:(uint64_t)address return -1; // Address not found } -- (nullable NSString *)pathForInAppInclude:(NSString *)inAppInclude +- (NSSet *)imagePathsForInAppInclude:(NSString *)inAppInclude { + NSMutableSet *imagePaths = [NSMutableSet new]; + @synchronized(self) { for (SentryBinaryImageInfo *info in _cache) { if ([SentryInAppLogic isImageNameInApp:info.name inAppInclude:inAppInclude]) { - return info.name; + [imagePaths addObject:info.name]; } } } - return nil; + return imagePaths; } @end diff --git a/Sources/Sentry/SentrySubClassFinder.m b/Sources/Sentry/SentrySubClassFinder.m index 84bea71325..261d7596b0 100644 --- a/Sources/Sentry/SentrySubClassFinder.m +++ b/Sources/Sentry/SentrySubClassFinder.m @@ -49,6 +49,8 @@ - (void)actOnSubclassesOfViewControllerInImage:(NSString *)imageName block:(void copyClassNamesForImage:[imageName cStringUsingEncoding:NSUTF8StringEncoding] amount:&count]; + SENTRY_LOG_DEBUG(@"Found %u number of classes in image: %@.", count, imageName); + // Storing the actual classes in an NSArray would call initializer of the class, which we // must avoid as we are on a background thread here and dealing with UIViewControllers, // which assume they are running on the main thread. Therefore, we store the class name @@ -87,9 +89,9 @@ - (void)actOnSubclassesOfViewControllerInImage:(NSString *)imageName block:(void block(NSClassFromString(className)); } - SENTRY_LOG_DEBUG( - @"The following UIViewControllers will generate automatic transactions: %@", - [classesToSwizzle componentsJoinedByString:@", "]); + SENTRY_LOG_DEBUG(@"The following UIViewControllers for image: %@ will generate " + @"automatic transactions: %@", + imageName, [classesToSwizzle componentsJoinedByString:@", "]); }]; }]; } diff --git a/Sources/Sentry/SentryUIViewControllerSwizzling.m b/Sources/Sentry/SentryUIViewControllerSwizzling.m index 6b27feaa4b..da9bd4f910 100644 --- a/Sources/Sentry/SentryUIViewControllerSwizzling.m +++ b/Sources/Sentry/SentryUIViewControllerSwizzling.m @@ -79,12 +79,17 @@ - (instancetype)initWithOptions:(SentryOptions *)options - (void)start { for (NSString *inAppInclude in self.inAppLogic.inAppIncludes) { - NSString *pathToImage = [self.binaryImageCache pathForInAppInclude:inAppInclude]; - if (pathToImage != nil) { - [self swizzleUIViewControllersOfImage:pathToImage]; + NSSet *imagePathsToInAppInclude = + [self.binaryImageCache imagePathsForInAppInclude:inAppInclude]; + + if (imagePathsToInAppInclude.count > 0) { + for (NSString *imagePath in imagePathsToInAppInclude) { + [self swizzleUIViewControllersOfImage:imagePath]; + } } else { - SENTRY_LOG_WARN(@"Failed to find the binary image for inAppInclude <%@> and, therefore " - @"can't instrument UIViewControllers in that binary", + SENTRY_LOG_WARN( + @"Failed to find the binary image(s) for inAppInclude <%@> and, therefore " + @"can't instrument UIViewControllers in these binaries.", inAppInclude); } } diff --git a/Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h b/Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h index fe743f265e..e4278a5126 100644 --- a/Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h +++ b/Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable SentryBinaryImageInfo *)imageByAddress:(const uint64_t)address; -- (nullable NSString *)pathForInAppInclude:(NSString *)inAppInclude; +- (NSSet *)imagePathsForInAppInclude:(NSString *)inAppInclude; + (NSString *_Nullable)convertUUID:(const unsigned char *const)value; diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift index fa4a7cad04..71c0fbc38c 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerSwizzlingTests.swift @@ -25,11 +25,6 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { cString: class_getImageName(SentryUIViewControllerSwizzlingTests.self)!, encoding: .utf8)! as NSString options.add(inAppInclude: imageName.lastPathComponent) - - let externalImageName = String( - cString: class_getImageName(ExternalUIViewController.self)!, - encoding: .utf8)! as NSString - options.add(inAppInclude: externalImageName.lastPathComponent) } var sut: SentryUIViewControllerSwizzling { @@ -167,6 +162,11 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { } func testSwizzlingOfExternalLibs() { + let externalImageName = String( + cString: class_getImageName(ExternalUIViewController.self)!, + encoding: .utf8)! as NSString + fixture.options.add(inAppInclude: externalImageName.lastPathComponent) + let sut = fixture.sut sut.start() let controller = ExternalUIViewController() @@ -174,6 +174,47 @@ class SentryUIViewControllerSwizzlingTests: XCTestCase { XCTAssertNotNil(SentrySDK.span) } + func testSwizzleInAppIncludes_WithShortenedInAppInclude() throws { + let imageName = try XCTUnwrap(String( + cString: class_getImageName(ExternalUIViewController.self)!, + encoding: .utf8) as? NSString) + + let lastPathComponent = String(imageName.lastPathComponent) + let shortenedLastPathComponent = String(lastPathComponent.prefix(5)) + + fixture.options.add(inAppInclude: shortenedLastPathComponent) + + let sut = fixture.sut + sut.start() + let controller = ExternalUIViewController() + controller.loadView() + XCTAssertNotNil(SentrySDK.span) + } + + /// Xcode 16 introduces a new flag ENABLE_DEBUG_DYLIB (https://developer.apple.com/documentation/xcode/build-settings-reference#Enable-Debug-Dylib-Support) + /// If this flag is enabled, debug builds of app and app extension targets on supported platforms and SDKs + /// will be built with the main binary code in a separate “NAME.debug.dylib”. + /// This test adds this debug.dylib and checks if it gets swizzled. + func testSwizzle_DebugDylib_GetsSwizzled() { + let imageName = String( + cString: class_getImageName(SentryUIViewControllerSwizzlingTests.self)!, + encoding: .utf8)! as NSString + + let debugDylib = "\(imageName).debug.dylib" + + var image = createCrashBinaryImage(0, name: debugDylib) + SentryDependencyContainer.sharedInstance().binaryImageCache.start() + SentryDependencyContainer.sharedInstance().binaryImageCache.binaryImageAdded(&image) + + let sut = fixture.sut + sut.start() + + let subClassFinderInvocations = fixture.subClassFinder.invocations + let result = subClassFinderInvocations.invocations.filter { $0.imageName == debugDylib } + + XCTAssertEqual(1, result.count) + } + func testSwizzle_fromScene_invalidNotification_NoObject() { let swizzler = fixture.testableSut diff --git a/Tests/SentryTests/SentryBinaryImageCacheTests.swift b/Tests/SentryTests/SentryBinaryImageCacheTests.swift index 4971731387..01f45da899 100644 --- a/Tests/SentryTests/SentryBinaryImageCacheTests.swift +++ b/Tests/SentryTests/SentryBinaryImageCacheTests.swift @@ -122,17 +122,17 @@ class SentryBinaryImageCacheTests: XCTestCase { sut.binaryImageAdded(&binaryImage) sut.binaryImageAdded(&binaryImage2) - let path = sut.pathFor(inAppInclude: "Expected Name at 0") - XCTAssertEqual(path, "Expected Name at 0") + let paths = sut.imagePathsFor(inAppInclude: "Expected Name at 0") + XCTAssertEqual(paths.first, "Expected Name at 0") - let path2 = sut.pathFor(inAppInclude: "Expected Name at 1") - XCTAssertEqual(path2, "Expected Name at 1") + let paths2 = sut.imagePathsFor(inAppInclude: "Expected Name at 1") + XCTAssertEqual(paths2.first, "Expected Name at 1") - let path3 = sut.pathFor(inAppInclude: "Expected") - XCTAssertEqual(path3, "Expected Name at 0") + let bothPaths = sut.imagePathsFor(inAppInclude: "Expected") + XCTAssertEqual(bothPaths, ["Expected Name at 0", "Expected Name at 1"]) - let didNotFind = sut.pathFor(inAppInclude: "Name at 0") - XCTAssertNil(didNotFind) + let didNotFind = sut.imagePathsFor(inAppInclude: "Name at 0") + XCTAssertTrue(didNotFind.isEmpty) } func testBinaryImageWithNULLName_DoesNotAddImage() { @@ -193,7 +193,7 @@ class SentryBinaryImageCacheTests: XCTestCase { for i in 0.. SentryCrashBinaryImage { - let name = "Expected Name at \(address)" - let nameCString = name.withCString { strdup($0) } - - var uuidPointer = UnsafeMutablePointer(nil) - let uuidAsCharArray: [UInt8] = [132, 186, 235, 218, 173, 26, 51, 244, 179, 93, 138, 69, 245, 218, 243, 34] - uuidPointer = UnsafeMutablePointer.allocate(capacity: uuidAsCharArray.count) - uuidPointer?.initialize(from: uuidAsCharArray, count: uuidAsCharArray.count) - - let binaryImage = SentryCrashBinaryImage( - address: UInt64(address), - vmAddress: vmAddress, - size: 100, - name: nameCString, - uuid: uuidPointer, - cpuType: 1, - cpuSubType: 1, - majorVersion: 1, - minorVersion: 0, - revisionVersion: 0, - crashInfoMessage: nil, - crashInfoMessage2: nil - ) - - return binaryImage - } - +func createCrashBinaryImage(_ address: UInt, vmAddress: UInt64 = 0, name: String? = nil) -> SentryCrashBinaryImage { + let imageName = name ?? "Expected Name at \(address)" + let nameCString = imageName.withCString { strdup($0) } + + var uuidPointer = UnsafeMutablePointer(nil) + let uuidAsCharArray: [UInt8] = [132, 186, 235, 218, 173, 26, 51, 244, 179, 93, 138, 69, 245, 218, 243, 34] + uuidPointer = UnsafeMutablePointer.allocate(capacity: uuidAsCharArray.count) + uuidPointer?.initialize(from: uuidAsCharArray, count: uuidAsCharArray.count) + + let binaryImage = SentryCrashBinaryImage( + address: UInt64(address), + vmAddress: vmAddress, + size: 100, + name: nameCString, + uuid: uuidPointer, + cpuType: 1, + cpuSubType: 1, + majorVersion: 1, + minorVersion: 0, + revisionVersion: 0, + crashInfoMessage: nil, + crashInfoMessage2: nil + ) + + return binaryImage }