Skip to content

Commit 46a05d1

Browse files
Merge pull request #2224 from DataDog/mariedm/rum-8415-create-swiftui-view-predicate
RUM-8415 Add SwiftUI view auto-instrumentation Co-authored-by: mariedm <marie.denis@datadoghq.com>
2 parents 24720ef + 4514841 commit 46a05d1

File tree

17 files changed

+804
-81
lines changed

17 files changed

+804
-81
lines changed

Datadog/Datadog.xcodeproj/project.pbxproj

+54
Large diffs are not rendered by default.

DatadogInternal/Sources/Utils/CustomDump.swift

+12-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
*/
3030

3131
#if DEBUG
32-
3332
// swiftlint:disable function_default_parameter_at_end
3433

3534
import Foundation
@@ -49,7 +48,7 @@ import Foundation
4948
/// components. The default is `Int.max`.
5049
/// - Returns: The instance passed as `value`.
5150
@discardableResult
52-
internal func customDump<T>(
51+
public func customDump<T>(
5352
_ value: T,
5453
name: String? = nil,
5554
indent: Int = 0,
@@ -550,7 +549,17 @@ public struct FileHandlerOutputStream: TextOutputStream {
550549
}
551550
}
552551
}
553-
554552
// swiftlint:enable function_default_parameter_at_end
555553

554+
/// Dumps the given value's contents in a file.
555+
public func dump<T>(_ value: T, filename: String) throws {
556+
let manager = FileManager.default
557+
let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(filename) //swiftlint:disable:this force_unwrapping
558+
manager.createFile(atPath: url.path, contents: nil, attributes: nil)
559+
let handle = try FileHandle(forWritingTo: url)
560+
var stream = FileHandlerOutputStream(handle)
561+
customDump(value, to: &stream)
562+
print("Dump:", url)
563+
handle.closeFile()
564+
}
556565
#endif

DatadogInternal/Sources/Utils/ReflectionMirror.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ extension ReflectionMirror {
247247
return descendant(paths: &paths)
248248
}
249249

250-
func descendant(_ paths: [Path]) -> Any? {
250+
public func descendant(_ paths: [Path]) -> Any? {
251251
var paths = paths
252252
return descendant(paths: &paths)
253253
}

DatadogRUM/Sources/Instrumentation/RUMInstrumentation.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,19 @@ internal final class RUMInstrumentation: RUMCommandPublisher {
5959
watchdogTermination: WatchdogTerminationMonitor?,
6060
memoryWarningMonitor: MemoryWarningMonitor
6161
) {
62+
// MARK: TODO: RUM-8416 - Remove after we add SwiftUI view instrumentation option
6263
// Always create views handler (we can't know if it will be used by SwiftUI instrumentation)
6364
// and only swizzle `UIViewController` if UIKit instrumentation is configured:
6465
let viewsHandler = RUMViewsHandler(
6566
dateProvider: dateProvider,
66-
predicate: uiKitRUMViewsPredicate,
67+
uiKitPredicate: uiKitRUMViewsPredicate,
68+
swiftUIPredicate: nil,
69+
swiftUIViewNameExtractor: nil,
6770
notificationCenter: notificationCenter
6871
)
6972
let viewControllerSwizzler: UIViewControllerSwizzler? = {
7073
do {
74+
// MARK: TODO: RUM-8416 - Check both predicates after we add SwiftUI view instrumentation option
7175
if uiKitRUMViewsPredicate != nil {
7276
return try UIViewControllerSwizzler(handler: viewsHandler)
7377
}
@@ -80,11 +84,13 @@ internal final class RUMInstrumentation: RUMCommandPublisher {
8084
return nil
8185
}()
8286

87+
// MARK: TODO: RUM-8420 - Remove after we add SwiftUI action instrumentation option
8388
// Always create actions handler (we can't know if it will be used by SwiftUI instrumentation)
8489
// and only swizzle `UIApplicationSwizzler` if UIKit instrumentation is configured:
8590
let actionsHandler = RUMActionsHandler(dateProvider: dateProvider, predicate: uiKitRUMActionsPredicate)
8691
let uiApplicationSwizzler: UIApplicationSwizzler? = {
8792
do {
93+
// MARK: TODO: RUM-8420 - Check both predicates after we add SwiftUI action instrumentation option
8894
if uiKitRUMActionsPredicate != nil {
8995
return try UIApplicationSwizzler(handler: actionsHandler)
9096
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
import DatadogInternal
8+
9+
/// A description of the RUM View returned from the predicate.
10+
public struct RUMView {
11+
/// The RUM View name, appearing as `VIEW NAME` in RUM Explorer.
12+
public var name: String
13+
14+
/// The RUM View path, appearing as `VIEW PATH GROUP` / `VIEW URL` in RUM Explorer.
15+
/// If set `nil`, the view controller class name will be used.
16+
public var path: String?
17+
18+
/// Additional attributes to associate with the RUM View.
19+
public var attributes: [AttributeKey: AttributeValue]
20+
21+
/// Whether this view is modal, but should not be tracked with `startView` and `stopView`
22+
/// When this is `true`, the view previous to this one will be stopped, but this one will not be
23+
/// started. When this view is dismissed, the previous view will be started.
24+
public var isUntrackedModal: Bool
25+
26+
/// Initializes the RUM View description.
27+
/// - Parameters:
28+
/// - path: the RUM View path, appearing as `PATH` in RUM Explorer.
29+
/// - attributes: additional attributes to associate with the RUM View.
30+
@available(*, deprecated, message: "This initializer is renamed to `init(name:attributes:)`.")
31+
public init(path: String, attributes: [AttributeKey: AttributeValue] = [:]) {
32+
self.name = path
33+
self.path = path
34+
self.attributes = attributes
35+
self.isUntrackedModal = false
36+
}
37+
38+
/// Initializes the RUM View description.
39+
/// - Parameters:
40+
/// - name: the RUM View name, appearing as `VIEW NAME` in RUM Explorer.
41+
/// - attributes: additional attributes to associate with the RUM View.
42+
/// - isUntrackedModal: true if this view is modal, but should not call startView / stopView.
43+
public init(name: String, attributes: [AttributeKey: AttributeValue] = [:], isUntrackedModal: Bool = false) {
44+
self.name = name
45+
self.path = nil // the "VIEW URL" will default to view controller class name
46+
self.attributes = attributes
47+
self.isUntrackedModal = isUntrackedModal
48+
}
49+
}

DatadogRUM/Sources/Instrumentation/Views/RUMViewsHandler.swift

+34-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Foundation
88
import UIKit
99
import DatadogInternal
1010

11+
// MARK: - RUMViewsHandler
1112
internal final class RUMViewsHandler {
1213
/// RUM representation of a View.
1314
private struct View {
@@ -33,9 +34,17 @@ internal final class RUMViewsHandler {
3334
/// The current date provider.
3435
private let dateProvider: DateProvider
3536

36-
/// `UIKit` view predicate. `nil`, if `UIKit` auto-instrumentations is
37+
/// `UIKit` view predicate. `nil` if `UIKit` auto-instrumentations is
3738
/// disabled.
38-
private let predicate: UIKitRUMViewsPredicate?
39+
private let uiKitPredicate: UIKitRUMViewsPredicate?
40+
41+
/// `SwiftUI` view predicate. `nil` if `SwiftUI` auto-instrumentations is
42+
/// disabled.
43+
private let swiftUIPredicate: SwiftUIRUMViewsPredicate?
44+
45+
/// `SwiftUI` view name extractor.
46+
/// Extracts `SwiftUI` view name from view hierarchy.
47+
private let swiftUIViewNameExtractor: SwiftUIViewNameExtractor?
3948

4049
/// The notification center where this handler observes following `UIApplication` notifications:
4150
/// - `.didEnterBackgroundNotification`
@@ -64,11 +73,15 @@ internal final class RUMViewsHandler {
6473
/// a set of `UIApplication` notifications.
6574
init(
6675
dateProvider: DateProvider,
67-
predicate: UIKitRUMViewsPredicate?,
76+
uiKitPredicate: UIKitRUMViewsPredicate?,
77+
swiftUIPredicate: SwiftUIRUMViewsPredicate?,
78+
swiftUIViewNameExtractor: SwiftUIViewNameExtractor?,
6879
notificationCenter: NotificationCenter
6980
) {
7081
self.dateProvider = dateProvider
71-
self.predicate = predicate
82+
self.uiKitPredicate = uiKitPredicate
83+
self.swiftUIPredicate = swiftUIPredicate
84+
self.swiftUIViewNameExtractor = swiftUIViewNameExtractor
7285
self.notificationCenter = notificationCenter
7386

7487
notificationCenter.addObserver(
@@ -196,14 +209,15 @@ internal final class RUMViewsHandler {
196209
}
197210
}
198211

212+
// MARK: - UIViewControllerHandler
199213
extension RUMViewsHandler: UIViewControllerHandler {
200214
func notify_viewDidAppear(viewController: UIViewController, animated: Bool) {
201215
let identity = ViewIdentifier(viewController)
202216
if let view = stack.first(where: { $0.identity == identity }) {
203217
// If the stack already contains the view controller, just restarts the view.
204218
// This prevents from calling the predicate when unnecessary.
205219
add(view: view)
206-
} else if let rumView = predicate?.rumView(for: viewController) {
220+
} else if let rumView = uiKitPredicate?.rumView(for: viewController) {
207221
add(
208222
view: .init(
209223
identity: identity,
@@ -214,6 +228,20 @@ extension RUMViewsHandler: UIViewControllerHandler {
214228
instrumentationType: .uikit
215229
)
216230
)
231+
} else if let swiftUIPredicate,
232+
let swiftUIViewNameExtractor,
233+
let rumViewName = swiftUIViewNameExtractor.extractName(from: viewController),
234+
let rumView = swiftUIPredicate.rumView(for: rumViewName) {
235+
add(
236+
view: .init(
237+
identity: identity,
238+
name: rumView.name,
239+
path: rumView.path ?? viewController.canonicalClassName,
240+
isUntrackedModal: rumView.isUntrackedModal,
241+
attributes: rumView.attributes,
242+
instrumentationType: .swiftui
243+
)
244+
)
217245
} else if #available(iOS 13, tvOS 13, *), viewController.isModalInPresentation {
218246
add(
219247
view: .init(
@@ -233,6 +261,7 @@ extension RUMViewsHandler: UIViewControllerHandler {
233261
}
234262
}
235263

264+
// MARK: - SwiftUIViewHandler
236265
extension RUMViewsHandler: SwiftUIViewHandler {
237266
/// Respond to a `SwiftUI.View.onAppear` event.
238267
///
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
/// Controller type enum to identify different SwiftUI hosting controllers
8+
internal enum ControllerType {
9+
case tabItem
10+
case hostingController
11+
case navigationController
12+
case modal
13+
case unknown
14+
15+
/// Determines the controller type from the class name
16+
init(className: String) {
17+
if className.contains("_TtGC7SwiftUI19UIHostingControllerVVS_7TabItem8RootView_") {
18+
self = .tabItem
19+
} else if className.contains("TtGC7SwiftUI19UIHostingController") {
20+
self = .hostingController
21+
} else if className.contains("Navigation") {
22+
self = .navigationController
23+
} else if className.contains("_TtGC7SwiftUI29PresentationHostingController") {
24+
self = .modal
25+
} else {
26+
self = .unknown
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
/// Protocol defining the predicate for SwiftUI view tracking in RUM.
8+
///
9+
/// The SDK uses this predicate to determine whether a detected SwiftUI view should be tracked as a RUM view.
10+
/// When a SwiftUI view is detected, the SDK first extracts its name,
11+
/// then passes that name to this predicate to convert it into RUM view parameters or filter it out.
12+
///
13+
/// Implement this protocol to customize which SwiftUI views are tracked and how they appear in the RUM Explorer.
14+
public protocol SwiftUIRUMViewsPredicate {
15+
/// Converts an extracted SwiftUI view name into RUM view parameters, or filters it out.
16+
///
17+
/// - Parameter extractedViewName: The name of the SwiftUI view detected by the SDK.
18+
/// - Returns: RUM view parameters if the view should be tracked, or `nil` to ignore the view.
19+
func rumView(for extractedViewName: String) -> RUMView?
20+
}
21+
22+
/// Default implementation of `SwiftUIRUMViewsPredicate`.
23+
///
24+
/// This implementation tracks all detected SwiftUI views with their extracted names.
25+
/// The view name in RUM Explorer will match the name extracted from the SwiftUI view.
26+
public struct DefaultSwiftUIRUMViewsPredicate: SwiftUIRUMViewsPredicate {
27+
public func rumView(for extractedViewName: String) -> RUMView? {
28+
return RUMView(name: extractedViewName)
29+
}
30+
}

0 commit comments

Comments
 (0)