diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a1352431..f408c8dd91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add protocol for custom screenName for UIViewControllers (#4646) + ## 8.43.1-beta.0 ### Fixes diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 5c3e1de9a0..caecda4450 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -324,6 +324,7 @@ 63FE722220DA66EC00CDBAE8 /* SentryCrashJSONCodec_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FE71F920DA66EB00CDBAE8 /* SentryCrashJSONCodec_Tests.m */; }; 63FE722420DA66EC00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FE71FB20DA66EB00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m */; }; 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FE71FC20DA66EB00CDBAE8 /* SentryCrashFileUtils_Tests.m */; }; + 64F9571D2D12DA1A00324652 /* SentryViewControllerBreadcrumbTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F9571C2D12DA1800324652 /* SentryViewControllerBreadcrumbTracking.swift */; }; 69BEE6F72620729E006DF9DF /* UrlSessionDelegateSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69BEE6F62620729E006DF9DF /* UrlSessionDelegateSpy.swift */; }; 71F116E8F40D530BB68A2987 /* SentryCrashUUIDConversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F11CDEF5952DF5CC69AC74 /* SentryCrashUUIDConversion.h */; }; 7B0002322477F0520035FEF1 /* SentrySessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B0002312477F0520035FEF1 /* SentrySessionTests.m */; }; @@ -1348,6 +1349,7 @@ 63FE71F920DA66EB00CDBAE8 /* SentryCrashJSONCodec_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryCrashJSONCodec_Tests.m; sourceTree = ""; }; 63FE71FB20DA66EB00CDBAE8 /* SentryCrashMonitor_NSException_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryCrashMonitor_NSException_Tests.m; sourceTree = ""; }; 63FE71FC20DA66EB00CDBAE8 /* SentryCrashFileUtils_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryCrashFileUtils_Tests.m; sourceTree = ""; }; + 64F9571C2D12DA1800324652 /* SentryViewControllerBreadcrumbTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewControllerBreadcrumbTracking.swift; sourceTree = ""; }; 69BEE6F62620729E006DF9DF /* UrlSessionDelegateSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSessionDelegateSpy.swift; sourceTree = ""; }; 71F11CDEF5952DF5CC69AC74 /* SentryCrashUUIDConversion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SentryCrashUUIDConversion.h; sourceTree = ""; }; 7B0002312477F0520035FEF1 /* SentrySessionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionTests.m; sourceTree = ""; }; @@ -3994,6 +3996,7 @@ D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( + 64F9571C2D12DA1800324652 /* SentryViewControllerBreadcrumbTracking.swift */, D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */, @@ -4787,6 +4790,7 @@ 15360CCF2432777500112302 /* SentrySessionTracker.m in Sources */, 6334314320AD9AE40077E581 /* SentryMechanism.m in Sources */, 849B8F9C2C6E906900148E1F /* SentryUserFeedbackThemeConfiguration.swift in Sources */, + 64F9571D2D12DA1A00324652 /* SentryViewControllerBreadcrumbTracking.swift in Sources */, 63FE70D320DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.c in Sources */, 849B8F9D2C6E906900148E1F /* SentryUserFeedbackWidgetConfiguration.swift in Sources */, 639FCFA51EBC809A00778193 /* SentryStacktrace.m in Sources */, diff --git a/Sources/Sentry/SentryBreadcrumbTracker.m b/Sources/Sentry/SentryBreadcrumbTracker.m index 455e32bd80..5b8c149ef8 100644 --- a/Sources/Sentry/SentryBreadcrumbTracker.m +++ b/Sources/Sentry/SentryBreadcrumbTracker.m @@ -304,7 +304,7 @@ + (NSDictionary *)fetchInfoAboutViewController:(UIViewController *)controller { NSMutableDictionary *info = @{}.mutableCopy; - info[@"screen"] = [SwiftDescriptor getObjectClassName:controller]; + info[@"screen"] = [SwiftDescriptor getViewControllerClassName:controller]; if ([controller.navigationItem.title length] != 0) { info[@"title"] = controller.navigationItem.title; @@ -316,12 +316,12 @@ + (NSDictionary *)fetchInfoAboutViewController:(UIViewController *)controller if (controller.presentingViewController != nil) { info[@"presentingViewController"] = - [SwiftDescriptor getObjectClassName:controller.presentingViewController]; + [SwiftDescriptor getViewControllerClassName:controller.presentingViewController]; } if (controller.parentViewController != nil) { info[@"parentViewController"] = - [SwiftDescriptor getObjectClassName:controller.parentViewController]; + [SwiftDescriptor getViewControllerClassName:controller.parentViewController]; } if (controller.view.window != nil) { @@ -335,6 +335,7 @@ + (NSDictionary *)fetchInfoAboutViewController:(UIViewController *)controller return info; } + #endif // SENTRY_HAS_UIKIT @end diff --git a/Sources/Sentry/SentryTimeToDisplayTracker.m b/Sources/Sentry/SentryTimeToDisplayTracker.m index 242ad76db5..f21c0a018e 100644 --- a/Sources/Sentry/SentryTimeToDisplayTracker.m +++ b/Sources/Sentry/SentryTimeToDisplayTracker.m @@ -44,7 +44,7 @@ - (instancetype)initForController:(UIViewController *)controller dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper { if (self = [super init]) { - _controllerName = [SwiftDescriptor getObjectClassName:controller]; + _controllerName = [SwiftDescriptor getViewControllerClassName:controller]; _waitForFullDisplay = waitForFullDisplay; _dispatchQueueWrapper = dispatchQueueWrapper; _initialDisplayReported = NO; diff --git a/Sources/Sentry/SentryUIApplication.m b/Sources/Sentry/SentryUIApplication.m index cfc1c95163..6e465ac701 100644 --- a/Sources/Sentry/SentryUIApplication.m +++ b/Sources/Sentry/SentryUIApplication.m @@ -122,12 +122,12 @@ - (UIApplication *)sharedApplication [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchSyncOnMainQueue:^{ - NSArray *viewControllers + NSArray *viewControllers = SentryDependencyContainer.sharedInstance.application.relevantViewControllers; NSMutableArray *vcsNames = [[NSMutableArray alloc] initWithCapacity:viewControllers.count]; - for (id vc in viewControllers) { - [vcsNames addObject:[SwiftDescriptor getObjectClassName:vc]]; + for (UIViewController *vc in viewControllers) { + [vcsNames addObject:[SwiftDescriptor getViewControllerClassName:vc]]; } result = [NSArray arrayWithArray:vcsNames]; } diff --git a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m index 513f2a6653..39e3430e8e 100644 --- a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m +++ b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m @@ -126,7 +126,7 @@ - (void)startRootSpanFor:(UIViewController *)controller [self.currentTTDTracker finishSpansIfNotFinished]; } - NSString *name = [SwiftDescriptor getObjectClassName:controller]; + NSString *name = [SwiftDescriptor getViewControllerClassName:controller]; spanId = [self.tracker startSpanWithName:name nameSource:kSentryTransactionNameSourceComponent operation:SentrySpanOperationUILoad diff --git a/Sources/Sentry/SentryViewHierarchy.m b/Sources/Sentry/SentryViewHierarchy.m index c7cc073d57..7bcc164838 100644 --- a/Sources/Sentry/SentryViewHierarchy.m +++ b/Sources/Sentry/SentryViewHierarchy.m @@ -144,7 +144,7 @@ - (int)viewHierarchyFromView:(UIView *)view intoContext:(SentryCrashJSONEncodeCo UIViewController *vc = (UIViewController *)view.nextResponder; if (vc.view == view) { const char *viewControllerClassName = - [[SwiftDescriptor getObjectClassName:vc] UTF8String]; + [[SwiftDescriptor getViewControllerClassName:vc] UTF8String]; tryJson(sentrycrashjson_addStringElement(context, "view_controller", viewControllerClassName, SentryCrashJSON_SIZE_AUTOMATIC)); } diff --git a/Sources/Swift/Protocol/SentryViewControllerBreadcrumbTracking.swift b/Sources/Swift/Protocol/SentryViewControllerBreadcrumbTracking.swift new file mode 100644 index 0000000000..4233efa579 --- /dev/null +++ b/Sources/Swift/Protocol/SentryViewControllerBreadcrumbTracking.swift @@ -0,0 +1,14 @@ +import Foundation + +/// +/// Use this protocol to customize the name used in the automatic +/// UIViewController performance tracker, view hierarchy, and breadcrumbs. +/// +@objc +public protocol SentryUIViewControllerDescriptor: NSObjectProtocol { + + /// The custom name of the UIViewController + /// that the Sentry SDK uses for transaction names, breadcrumbs, and + /// view hierarchy. + var sentryName: String { get } +} diff --git a/Sources/Swift/SwiftDescriptor.swift b/Sources/Swift/SwiftDescriptor.swift index 0d345c9ced..6d6e996857 100644 --- a/Sources/Swift/SwiftDescriptor.swift +++ b/Sources/Swift/SwiftDescriptor.swift @@ -1,5 +1,9 @@ import Foundation +#if canImport(UIKit) && !SENTRY_NO_UIKIT +import UIKit +#endif + @objc class SwiftDescriptor: NSObject { @@ -7,7 +11,18 @@ class SwiftDescriptor: NSObject { static func getObjectClassName(_ object: AnyObject) -> String { return String(describing: type(of: object)) } - + + /// UIViewControllers aren't available on watchOS +#if canImport(UIKit) && !os(watchOS) && !SENTRY_NO_UIKIT + @objc + static func getViewControllerClassName(_ object: UIViewController) -> String { + if let object = object as? SentryUIViewControllerDescriptor { + return object.sentryName + } + return getObjectClassName(object) + } +#endif + @objc static func getSwiftErrorDescription(_ error: Error) -> String? { return String(describing: error) diff --git a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift index 4e166c4f7d..f28f689ac8 100644 --- a/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Breadcrumbs/SentryBreadcrumbTrackerTests.swift @@ -287,7 +287,45 @@ class SentryBreadcrumbTrackerTests: XCTestCase { XCTAssertEqual(crumbData["accessibilityIdentifier"] as? String, "TestAccessibilityIdentifier") } - + + func testBreadcrumbViewControllerCustomScreenName() throws { + let testReachability = TestSentryReachability() + SentryDependencyContainer.sharedInstance().reachability = testReachability + + let scope = Scope() + let client = TestClient(options: Options()) + let hub = TestHub(client: client, andScope: scope) + SentrySDK.setCurrentHub(hub) + + let sut = SentryBreadcrumbTracker() + sut.start(with: delegate) + sut.startSwizzle() + + let parentScreenName = UUID().uuidString + let screenName = UUID().uuidString + let title = UUID().uuidString + + let parentController = CustomScreenNameViewController(sentryName: parentScreenName) + let viewController = CustomScreenNameViewController(sentryName: screenName) + parentController.addChild(viewController) + viewController.title = title + + viewController.viewDidAppear(false) + + let crumbs = delegate.addCrumbInvocations.invocations + + // one breadcrumb for starting the tracker, one for the first reachability breadcrumb and one final one for the swizzled viewDidAppear + guard crumbs.count == 2 else { + XCTFail("Expected exactly 2 breadcrumbs, got: \(crumbs)") + return + } + + let lifeCycleCrumb = try XCTUnwrap(crumbs.element(at: 1)) + XCTAssertEqual(screenName, lifeCycleCrumb.data?["screen"] as? String) + XCTAssertEqual(title, lifeCycleCrumb.data?["title"] as? String) + XCTAssertEqual(parentScreenName, lifeCycleCrumb.data?["parentViewController"] as? String) + } + private class TestEvent: UIEvent { let touchedView: UIView? class TestEndTouch: UITouch { @@ -307,7 +345,21 @@ class SentryBreadcrumbTrackerTests: XCTestCase { TestEndTouch(touchedView: touchedView) ] } } - + + fileprivate final class CustomScreenNameViewController: UIViewController, SentryUIViewControllerDescriptor { + + fileprivate required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate var sentryName: String + + fileprivate init(sentryName: String) { + self.sentryName = sentryName + super.init(nibName: nil, bundle: nil) + } + } + #endif }