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

RUMM-1097 Fix DefaultUIKitRUMViewsPredicate for Objc view controllers #419

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@
61D6FF7E24E53D3B00D0E375 /* BenchmarkMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */; };
61D980BA24E28D0100E03345 /* RUMIntegrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D980B924E28D0100E03345 /* RUMIntegrations.swift */; };
61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D980BB24E293F600E03345 /* RUMIntegrationsTests.swift */; };
61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */; };
61DC6D922539E3E300FFAA22 /* LoggingCommonAsserts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61DC6D912539E3E300FFAA22 /* LoggingCommonAsserts.swift */; };
61E36A11254B2280001AD6F2 /* LaunchTimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E36A10254B2280001AD6F2 /* LaunchTimeProvider.swift */; };
61E45BCF2450A6EC00F2C652 /* TracingUUIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */; };
Expand Down Expand Up @@ -772,6 +773,8 @@
61D6FF7D24E53D3B00D0E375 /* BenchmarkMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkMocks.swift; sourceTree = "<group>"; };
61D980B924E28D0100E03345 /* RUMIntegrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMIntegrations.swift; sourceTree = "<group>"; };
61D980BB24E293F600E03345 /* RUMIntegrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMIntegrationsTests.swift; sourceTree = "<group>"; };
61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomObjcViewController.h; sourceTree = "<group>"; };
61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomObjcViewController.m; sourceTree = "<group>"; };
61DC6D912539E3E300FFAA22 /* LoggingCommonAsserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingCommonAsserts.swift; sourceTree = "<group>"; };
61E36A10254B2280001AD6F2 /* LaunchTimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTimeProvider.swift; sourceTree = "<group>"; };
61E45BCE2450A6EC00F2C652 /* TracingUUIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingUUIDTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1362,6 +1365,8 @@
61F1A622249B811200075390 /* Encoding.swift */,
61A763DA252DB2B3005A23F2 /* NSURLSessionBridge.h */,
61A763DB252DB2B3005A23F2 /* NSURLSessionBridge.m */,
61DB33B025DEDFC200F7EA71 /* CustomObjcViewController.h */,
61DB33B125DEDFC200F7EA71 /* CustomObjcViewController.m */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -2951,6 +2956,7 @@
61133C5A2423990D00786299 /* FileTests.swift in Sources */,
61AD4E3A24534075006E34EA /* TracingFeatureTests.swift in Sources */,
61133C6B2423990D00786299 /* LogMatcher.swift in Sources */,
61DB33B225DEDFC200F7EA71 /* CustomObjcViewController.m in Sources */,
61F3CDAD25122C9200C816E5 /* UIKitRUMViewsHandlerTests.swift in Sources */,
61D980BC24E293F600E03345 /* RUMIntegrationsTests.swift in Sources */,
61363D9F24D99BAA0084CD6F /* DDErrorTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@
</EnvironmentVariable>
<EnvironmentVariable
key = "DD_TEST_SCENARIO_CLASS_NAME"
value = "RUMResourcesScenario"
value = "RUMURLSessionResourcesScenario"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "DD_TEST_SCENARIO_CLASS_NAME"
value = "RUMNSURLSessionResourcesScenario"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
Expand Down
17 changes: 15 additions & 2 deletions Datadog/Example/Scenarios/RUM/RUMScenarios.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,25 @@ final class RUMTapActionScenario: TestScenario {

/// Scenario which uses RUM and Tracing auto instrumentation features to track bunch of network requests
/// sent with `URLSession` from two VCs. The first VC calls first party resources, the second one calls third parties.
final class RUMResourcesScenario: URLSessionBaseScenario, TestScenario {
final class RUMURLSessionResourcesScenario: URLSessionBaseScenario, TestScenario {
static let storyboardName = "URLSessionScenario"

override func configureSDK(builder: Datadog.Configuration.Builder) {
_ = builder
.trackUIKitRUMViews(using: DefaultUIKitRUMViewsPredicate())
.trackUIKitRUMViews()

super.configureSDK(builder: builder) // applies the `trackURLSession(firstPartyHosts:)`
}
}

/// Scenario which uses RUM and Tracing auto instrumentation features to track bunch of network requests
/// sent with `NSURLSession` from two VCs. The first VC calls first party resources, the second one calls third parties.
final class RUMNSURLSessionResourcesScenario: URLSessionBaseScenario, TestScenario {
static let storyboardName = "NSURLSessionScenario"

override func configureSDK(builder: Datadog.Configuration.Builder) {
_ = builder
.trackUIKitRUMViews()

super.configureSDK(builder: builder) // applies the `trackURLSession(firstPartyHosts:)`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Datadog
internal class TrackingConsentBaseScenario {
func configureSDK(builder: Datadog.Configuration.Builder) {
_ = builder
.trackUIKitRUMViews(using: DefaultUIKitRUMViewsPredicate())
.trackUIKitRUMViews()
.trackUIKitActions(true)
.trackURLSession(firstPartyHosts: ["datadoghq.com"])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
*/

#import "NSURLSessionBridge.h"
#import "CustomObjcViewController.h"
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,20 @@ public struct DefaultUIKitRUMViewsPredicate: UIKitRUMViewsPredicate {
public init () {}

public func rumView(for viewController: UIViewController) -> RUMView? {
let canonicalClassName = viewController.canonicalClassName
let isCustomClass = canonicalClassName.contains(".") // custom class contains module prefix

if isCustomClass {
var view = RUMView(name: canonicalClassName)
view.path = canonicalClassName
return view
} else {
guard !isUIKit(class: type(of: viewController)) else {
// do not track bare UIKit's view controllers (UINavigationController, UITabBarController, ...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the real goal is excluding container view controllers, exclusion of UIKit types is just the workaround that we could come up with.
this comment here may be misleading if we come back to this issue later on, can we correct it?

here is the same issue in PR's description as well:

...This was to ignore all the "system" view controllers...

Copy link
Member Author

@ncreated ncreated Feb 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's about plain (not subclassed / customized) VCs from UIKit.

Before this PR, this implementation was considering MyCustomNavigationViewController as the RUM View. I think we shouldn't change that default if no one has reported it being wrong. We're doing a bug fix here, I wouldn't change the previous behaviour.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no i'm not talking about the behavior, my concern is that the comments don't tell the whole story.
we first wanted to exclude containers and then noticed invisible view controllers in the hiearchy. if one asks "why do we exclude UIKit types?", the answer is not obvious without this story.

IMO this comment do not track UIKit controllers brings questions (why?) rather than answering

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see 👍. Improved it with discussing the reasoning and optimisation.

return nil
}

let canonicalClassName = viewController.canonicalClassName
var view = RUMView(name: canonicalClassName)
view.path = canonicalClassName
return view
}

/// If given `class` comes from UIKit framework.
private func isUIKit(`class`: AnyClass) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Put this as an internal extension on Bundle in the UIKitExtensions.swift file

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean sth like Bundle.isUIKit(class:) -> Bool? This would read weird, no? This seems to be rather a characteristic of the AnyClass than a Bundle - but as AnyClass is a metatype it cannot be extended.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not important for me but i was thinking of smth like this:

// extension
extension AnyObject {
  var isUIKit: Bool { ... }
}
// calling site
if viewController.isUIKit { return }
else { ... }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👌 , I added:

internal extension Bundle {
    var isUIKit: Bool
}

extension and bunch of fixtures in tests.

let bundle = Bundle(for: `class`)
return bundle.bundleURL.lastPathComponent == "UIKitCore.framework"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,34 @@ private extension ExampleApplication {
}

class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts {
func testRUMResourcesScenario() throws {
private struct Expectations {
let expectedFirstPartyRequestsViewControllerName: String
let expectedThirdPartyRequestsViewControllerName: String
}

func testRUMURLSessionResourcesScenario() throws {
try runTest(
for: "RUMURLSessionResourcesScenario",
expectations: Expectations(
expectedFirstPartyRequestsViewControllerName: "Example.SendFirstPartyRequestsViewController",
expectedThirdPartyRequestsViewControllerName: "Example.SendThirdPartyRequestsViewController"
)
)
}

func testRUMNSURLSessionResourcesScenario() throws {
try runTest(
for: "RUMNSURLSessionResourcesScenario",
expectations: Expectations(
expectedFirstPartyRequestsViewControllerName: "ObjcSendFirstPartyRequestsViewController",
expectedThirdPartyRequestsViewControllerName: "ObjcSendThirdPartyRequestsViewController"
)
)
}

/// Both, `URLSession` (Swift) and `NSURLSession` (Objective-C) scenarios use different storyboards
/// and different view controllers to run this test, but the the logic and the instrumentation is the same.
private func runTest(for testScenarioClassName: String, expectations: Expectations) throws {
// Server session recording first party requests send to `HTTPServerMock`.
// Used to assert that trace propagation headers are send for first party requests.
let customFirstPartyServerSession = server.obtainUniqueRecordingSession()
Expand All @@ -40,7 +67,7 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts {

let app = ExampleApplication()
app.launchWith(
testScenarioClassName: "RUMResourcesScenario",
testScenarioClassName: testScenarioClassName,
serverConfiguration: HTTPServerMockConfiguration(
tracesEndpoint: tracingServerSession.recordingURL,
rumEndpoint: rumServerSession.recordingURL,
Expand Down Expand Up @@ -88,7 +115,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts {
XCTAssertEqual(session.viewVisits.count, 2)

// Asserts in `SendFirstPartyRequestsVC` RUM View
XCTAssertEqual(session.viewVisits[0].path, "Example.SendFirstPartyRequestsViewController")
XCTAssertEqual(session.viewVisits[0].name, expectations.expectedFirstPartyRequestsViewControllerName)
XCTAssertEqual(session.viewVisits[0].path, expectations.expectedFirstPartyRequestsViewControllerName)
XCTAssertEqual(session.viewVisits[0].resourceEvents.count, 2, "1st screen should track 2 RUM Resources")
XCTAssertEqual(session.viewVisits[0].errorEvents.count, 1, "1st screen should track 1 RUM Errors")

Expand Down Expand Up @@ -126,7 +154,8 @@ class RUMResourcesScenarioTests: IntegrationTests, RUMCommonAsserts {
XCTAssertEqual(firstPartyResourceError1.error.resource?.method, .get)

// Asserts in `SendThirdPartyRequestsVC` RUM View
XCTAssertEqual(session.viewVisits[1].path, "Example.SendThirdPartyRequestsViewController")
XCTAssertEqual(session.viewVisits[1].name, expectations.expectedThirdPartyRequestsViewControllerName)
XCTAssertEqual(session.viewVisits[1].path, expectations.expectedThirdPartyRequestsViewControllerName)
XCTAssertEqual(session.viewVisits[1].resourceEvents.count, 2, "2nd screen should track 2 RUM Resources")
XCTAssertEqual(session.viewVisits[1].errorEvents.count, 0, "2nd screen should track no RUM Errors")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@ import XCTest
import Datadog

class UIKitRUMViewsPredicateTests: XCTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also add test cases to check that some known UIKit classes are filtered out, to catch any regressions of the internal isUIKit with future releases or iOS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to @acgh , we rely on private information (UIKitCore is private) and that may break at any time

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already there - just not visible in this diff. We had this test before:

    func testGivenDefaultPredicate_whenAskingUIKitViewController_itReturnsNoView() {
        // Given
        let predicate = DefaultUIKitRUMViewsPredicate()
        // When
        let uiKitViewController = UIViewController()
        let rumView = predicate.rumView(for: uiKitViewController)
        // Then
        XCTAssertNil(rumView)
    }

func testGivenDefaultPredicate_whenAskingForCustomViewController_itNamesTheViewByItsClassName() {
func testGivenDefaultPredicate_whenAskingForCustomSwiftViewController_itNamesTheViewByItsClassName() {
// Given
let predicate = DefaultUIKitRUMViewsPredicate()

// When
let customViewController = createMockView(viewControllerClassName: "Module.CustomViewController")
let customViewController = createMockView(viewControllerClassName: "CustomSwiftViewController")
let rumView = predicate.rumView(for: customViewController)

// Then
XCTAssertEqual(rumView?.path, "Module.CustomViewController")
XCTAssertEqual(rumView?.name, "CustomSwiftViewController")
XCTAssertEqual(rumView?.path, "CustomSwiftViewController")
XCTAssertTrue(rumView!.attributes.isEmpty)
}

func testGivenDefaultPredicate_whenAskingForCustomObjcViewController_itNamesTheViewByItsClassName() {
// Given
let predicate = DefaultUIKitRUMViewsPredicate()

// When
let customViewController = CustomObjcViewController()
let rumView = predicate.rumView(for: customViewController)

// Then
XCTAssertEqual(rumView?.name, "CustomObjcViewController")
XCTAssertEqual(rumView?.path, "CustomObjcViewController")
XCTAssertTrue(rumView!.attributes.isEmpty)
}

Expand Down
15 changes: 15 additions & 0 deletions Tests/DatadogTests/Helpers/CustomObjcViewController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface CustomObjcViewController : UIViewController

@end

NS_ASSUME_NONNULL_END
19 changes: 19 additions & 0 deletions Tests/DatadogTests/Helpers/CustomObjcViewController.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2019-2020 Datadog, Inc.
*/

#import "CustomObjcViewController.h"

@interface CustomObjcViewController ()

@end

@implementation CustomObjcViewController

- (void)viewDidLoad {
[super viewDidLoad];
}

@end