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-1615 SwiftUI View Instrumentation #645

Merged
merged 8 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
96 changes: 76 additions & 20 deletions Datadog/Datadog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
value = "LoggingManualInstrumentationScenario"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "DD_TEST_SCENARIO_CLASS_NAME"
value = "RUMSwiftUIInstrumentationScenario"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "DD_TEST_SCENARIO_CLASS_NAME"
value = "RUMMobileVitalsScenario"
Expand Down
26 changes: 26 additions & 0 deletions Datadog/Example/Scenarios/RUM/RUMScenarios.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,32 @@ final class RUMScrubbingScenario: TestScenario {
}
}

@available(iOS 13, *)
/// Scenario which presents `SwiftUI`-based hierarchy and navigates through
/// its views and view controllers.
final class RUMSwiftUIInstrumentationScenario: TestScenario {
static var storyboardName: String = "RUMSwiftUIInstrumentationScenario"
maxep marked this conversation as resolved.
Show resolved Hide resolved

private class Predicate: UIKitRUMViewsPredicate {
let `default` = DefaultUIKitRUMViewsPredicate()

func rumView(for viewController: UIViewController) -> RUMView? {
if viewController is SwiftUIRootViewController {
return nil
}

return `default`.rumView(for: viewController)
}
}

func configureSDK(builder: Datadog.Configuration.Builder) {
_ = builder
.trackUIKitRUMViews(using: Predicate())
.enableLogging(false)
.enableTracing(false)
}
}

// MARK: - Helpers

private func rumResourceAttributesProvider(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
<device id="retina6_1" orientation="portrait" appearance="dark"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--SwiftUI Root View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController id="Y6W-OH-hqX" customClass="SwiftUIRootViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139" y="125"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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 Foundation
import SwiftUI
import Datadog

@available(iOS 13, *)
/// A custom SwiftUI Hosting controller for `RootView`.
///
/// This definition only exist to allow instantiation from `RUMSwiftUIInstrumentationScenario`
/// storyboard and should be ignored from RUM instrumentation.
class SwiftUIRootViewController: UIHostingController<RootView> {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder, rootView: RootView())
}
}

@available(iOS 13, *)
/// The root view of the SwiftUI instrumentation test.
///
/// This view creates a navigation stack and present a`ScreenView` as fist view.
struct RootView: View {
var body: some View {
TabView {
tabNavigationView
.tabItem {
Text("Navigation View")
}

tabScreenView
.tabItem {
Text("Screen 100")
}
}
}

@ViewBuilder
var tabNavigationView: some View {
// An issue was introduced in iOS 14.2 (FB8907671) which makes
// `TabView` items to be loaded twice, once when the `TabView`
// appears` and once when the Tab item itself appears. This
// lead to RUM views being reported twice. This issue was fixed
// in iOS 14.5, see https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14_5-release-notes
// As a workaround, the tab view items can be embedded in a
// `LazyVStack` or `LazyHStack` to lazily load its content when
// it needs to render them onscreen.
if #available(iOS 14.5, *) {
NavigationView {
ScreenView(index: 1)
}
} else if #available(iOS 14.2, *) {
NavigationView {
LazyVStack {
ScreenView(index: 1)
}
}
} else {
NavigationView {
ScreenView(index: 1)
}
}
}

@ViewBuilder
var tabScreenView: some View {
if #available(iOS 14.5, *) {
ScreenView(index: 100)
} else if #available(iOS 14.2, *) {
LazyVStack {
ScreenView(index: 100)
}
} else {
ScreenView(index: 100)
}
}
}

@available(iOS 13, *)
/// A basic Screen View at a given index in the stack.
///
/// This view presents a single navigation button to push a
/// `UIScreenView` onto the stack.
struct ScreenView: View {

/// The view index in the stack.
let index: Int

@State private var presentSheet = false

var body: some View {
VStack(spacing: 32) {
NavigationLink("Push to Next View", destination:
ScreenView(index: index + 1)
)
Button("Present Modal View") {
presentSheet.toggle()
}
}
.sheet(isPresented: $presentSheet) {
NavigationView {
destination
}
}
.navigationBarTitle("Screen \(index)")
.trackRUMView(name: "SwiftUI View \(index)")
}

@ViewBuilder
var destination: some View {
ScreenView(index: index + 1)
}
}
14 changes: 14 additions & 0 deletions Sources/Datadog/RUM/Instrumentation/RUMInstrumentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ internal final class RUMInstrumentation: RUMCommandPublisher {

/// RUM Views auto instrumentation, `nil` if not enabled.
let viewsAutoInstrumentation: ViewsAutoInstrumentation?
/// `SwiftUI.View` RUM instrumentation
let swiftUIViewInstrumentation: SwiftUIViewHandler
/// RUM User Actions auto instrumentation, `nil` if not enabled.
let userActionsAutoInstrumentation: UserActionsAutoInstrumentation?
/// RUM Long Tasks auto instrumentation, `nil` if not enabled.
Expand Down Expand Up @@ -84,6 +86,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher {
self.viewsAutoInstrumentation = viewsAutoInstrumentation
self.userActionsAutoInstrumentation = userActionsAutoInstrumentation
self.longTasks = longTasks
self.swiftUIViewInstrumentation = SwiftUIRUMViewsHandler(dateProvider: dateProvider)
}

func enable() {
Expand All @@ -96,6 +99,7 @@ internal final class RUMInstrumentation: RUMCommandPublisher {
viewsAutoInstrumentation?.handler.publish(to: subscriber)
userActionsAutoInstrumentation?.handler.publish(to: subscriber)
longTasks?.publish(to: subscriber)
swiftUIViewInstrumentation.publish(to: subscriber)
}

#if DD_SDK_COMPILED_FOR_TESTING
Expand All @@ -107,3 +111,13 @@ internal final class RUMInstrumentation: RUMCommandPublisher {
}
#endif
}

extension RUMInstrumentation: SwiftUIViewHandler {
func onAppear(identity: String, name: String, path: String, attributes: [AttributeKey: AttributeValue]) {
swiftUIViewInstrumentation.onAppear(identity: identity, name: name, path: path, attributes: attributes)
}

func onDisappear(identity: String) {
swiftUIViewInstrumentation.onDisappear(identity: identity)
}
}
maxep marked this conversation as resolved.
Show resolved Hide resolved
Loading