Skip to content

Commit 1128190

Browse files
feat: UIKit instrumentation now sets the current view (#26)
## Which problem is this PR solving? UIKit Instrumentation was emitting `viewDidAppear` and `viewDidDisappear` events, but wasn't persisting the "current screen" value as the SwiftUI Navigation instrumentation did in #23 ## Short description of the changes UIKit Instrumentation now tracks the "current screen" value Computing "current screen" is a little complex but tldr we use the following (in order of precedence): - `accessiblityIdentifier` - Storybook Identifier - `view.title` - current class name of the view controller `honeycombIdentifier` is provided as an escape hatch for clients to opt out of this precedence chain and set some explicit value This PR also updates our smoke test app to use nested UIKit navigations as a way to exercise the "current screen" computations. Note that we don't do anything in the `viewDidDisappear` event: I considered setting the current screen to `nil` but opted against that because 1. it seemed like there were cases where, View B is replacing View A, View A's `viewDidDisappear` would run _after_ View B's `viewDidAppear`. Thus setting current screen to `nil` would actually clobber the (now correct) value corresponding to whatever we get from View B 2. I reasoned that if one view is disappearing, then it must be replaced by some other view, and that other view is responsible to setting current screen (either automatically, if the new view is a UIKit view, or explicitly, if it's a SwiftUI view) ## How to verify that this has the expected result - [x] run the smoke test app and manually inspect collector output - [x] updated smoke tests
1 parent 72ada47 commit 1128190

File tree

8 files changed

+182
-24
lines changed

8 files changed

+182
-24
lines changed

Examples/SmokeTest/SmokeTest.xcodeproj/project.pbxproj

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10-
452B71DD2CE52C8600C27FB2 /* ViewInstrumentationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */; };
1110
366309102CE51BDC00B97612 /* UIKitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3663090F2CE51BDC00B97612 /* UIKitView.swift */; };
12-
366309122CE51EF000B97612 /* UIKitView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 366309112CE51EF000B97612 /* UIKitView.storyboard */; };
11+
366309122CE51EF000B97612 /* UIKitViewStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 366309112CE51EF000B97612 /* UIKitViewStoryboard.storyboard */; };
12+
452B71DD2CE52C8600C27FB2 /* ViewInstrumentationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */; };
1313
452B71E12CE6A52600C27FB2 /* NavigationExamplesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B71E02CE6A52600C27FB2 /* NavigationExamplesView.swift */; };
1414
AF6DEFEA2C8D3CE000363027 /* SmokeTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6DEFE92C8D3CE000363027 /* SmokeTestApp.swift */; };
1515
AF6DEFEC2C8D3CE000363027 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6DEFEB2C8D3CE000363027 /* ContentView.swift */; };
@@ -41,9 +41,9 @@
4141
/* End PBXContainerItemProxy section */
4242

4343
/* Begin PBXFileReference section */
44-
452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInstrumentationView.swift; sourceTree = "<group>"; };
4544
3663090F2CE51BDC00B97612 /* UIKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitView.swift; sourceTree = "<group>"; };
46-
366309112CE51EF000B97612 /* UIKitView.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = UIKitView.storyboard; sourceTree = "<group>"; };
45+
366309112CE51EF000B97612 /* UIKitViewStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = UIKitViewStoryboard.storyboard; sourceTree = "<group>"; };
46+
452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInstrumentationView.swift; sourceTree = "<group>"; };
4747
452B71E02CE6A52600C27FB2 /* NavigationExamplesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationExamplesView.swift; sourceTree = "<group>"; };
4848
AF6DEFE62C8D3CE000363027 /* SmokeTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmokeTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
4949
AF6DEFE92C8D3CE000363027 /* SmokeTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeTestApp.swift; sourceTree = "<group>"; };
@@ -118,7 +118,7 @@
118118
AF6DEFED2C8D3CE100363027 /* Assets.xcassets */,
119119
AF6DEFEF2C8D3CE100363027 /* Preview Content */,
120120
3663090F2CE51BDC00B97612 /* UIKitView.swift */,
121-
366309112CE51EF000B97612 /* UIKitView.storyboard */,
121+
366309112CE51EF000B97612 /* UIKitViewStoryboard.storyboard */,
122122
);
123123
path = SmokeTest;
124124
sourceTree = "<group>";
@@ -263,7 +263,7 @@
263263
buildActionMask = 2147483647;
264264
files = (
265265
AF6DEFF12C8D3CE100363027 /* Preview Assets.xcassets in Resources */,
266-
366309122CE51EF000B97612 /* UIKitView.storyboard in Resources */,
266+
366309122CE51EF000B97612 /* UIKitViewStoryboard.storyboard in Resources */,
267267
AF6DEFEE2C8D3CE100363027 /* Assets.xcassets in Resources */,
268268
);
269269
runOnlyForDeploymentPostprocessing = 0;

Examples/SmokeTest/SmokeTest/UIKitView.swift

+12-3
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,29 @@ struct UIKView_preview: PreviewProvider {
1616

1717
struct StoryboardViewControllerRepresentation: UIViewControllerRepresentable {
1818
func makeUIViewController(context: Context) -> some UIViewController {
19-
let storyboard = UIStoryboard(name: "UIKitView", bundle: Bundle.main)
20-
let controller = storyboard.instantiateViewController(identifier: "UIKitView")
19+
let storyboard = UIStoryboard(name: "UIKitViewStoryboard", bundle: Bundle.main)
20+
let controller = storyboard.instantiateViewController(identifier: "UIKitNavigationRoot")
2121
return controller
2222
}
2323

2424
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
2525
}
2626
}
2727

28-
class ViewController: UIViewController {
28+
class UIKitScreenViewController: UIViewController {
2929

3030
override func viewDidLoad() {
3131
super.viewDidLoad()
32+
self.title = "UIKit Screen"
3233
// Do any additional setup after loading the view.
3334
}
3435

3536
}
37+
38+
class UIKitMenuViewController: UIViewController {
39+
40+
override func viewDidLoad() {
41+
super.viewDidLoad()
42+
title = "UIKit Menu"
43+
}
44+
}

Examples/SmokeTest/SmokeTest/UIKitView.storyboard Examples/SmokeTest/SmokeTest/UIKitViewStoryboard.storyboard

+55-3
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
99
</dependencies>
1010
<scenes>
11-
<!--View Controller-->
11+
<!--Kit Screen View Controller-->
1212
<scene sceneID="s0d-6b-0kx">
1313
<objects>
14-
<viewController storyboardIdentifier="UIKitView" id="Y6W-OH-hqX" customClass="ViewController" sceneMemberID="viewController">
14+
<viewController id="Y6W-OH-hqX" customClass="UIKitScreenViewController" customModule="SmokeTest" customModuleProvider="target" sceneMemberID="viewController">
1515
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
1616
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
1717
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -60,6 +60,9 @@
6060
</subviews>
6161
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
6262
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
63+
<accessibility key="accessibilityConfiguration" identifier="UI KIT SCREEN OVERRIDE">
64+
<bool key="isElement" value="NO"/>
65+
</accessibility>
6366
<constraints>
6467
<constraint firstItem="vDu-zF-Fre" firstAttribute="trailing" secondItem="ks9-AV-zp6" secondAttribute="trailing" constant="16" id="34M-ZV-0ya"/>
6568
<constraint firstItem="zfX-ff-qIS" firstAttribute="top" secondItem="DzX-1X-ZOc" secondAttribute="bottom" constant="16" id="3eY-tS-YQ1"/>
@@ -80,10 +83,59 @@
8083
<constraint firstItem="008-k5-M1T" firstAttribute="centerX" secondItem="5EZ-qb-Rvc" secondAttribute="centerX" id="xun-d3-rYB"/>
8184
</constraints>
8285
</view>
86+
<navigationItem key="navigationItem" id="CFx-hq-SqH"/>
8387
</viewController>
8488
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
8589
</objects>
86-
<point key="canvasLocation" x="61.832061068702288" y="3.5211267605633805"/>
90+
<point key="canvasLocation" x="1915.2671755725189" y="3.5211267605633805"/>
91+
</scene>
92+
<!--Kit Menu View Controller-->
93+
<scene sceneID="WJl-a9-s4a">
94+
<objects>
95+
<viewController id="bfq-Fn-kGB" customClass="UIKitMenuViewController" customModule="SmokeTest" customModuleProvider="target" sceneMemberID="viewController">
96+
<view key="view" contentMode="scaleToFill" id="uMH-m2-CcE">
97+
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
98+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
99+
<subviews>
100+
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="fill" contentVerticalAlignment="fill" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7xf-19-0Op">
101+
<rect key="frame" x="0.0" y="401" width="393" height="51"/>
102+
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
103+
<edgeInsets key="layoutMargins" top="8" left="8" bottom="8" right="8"/>
104+
<state key="normal" title="Button"/>
105+
<buttonConfiguration key="configuration" style="plain" title="Go To UIKit App" buttonSize="large" titleAlignment="center">
106+
<fontDescription key="titleFontDescription" type="system" pointSize="20"/>
107+
</buttonConfiguration>
108+
<connections>
109+
<segue destination="Y6W-OH-hqX" kind="show" id="CMK-mH-lON"/>
110+
</connections>
111+
</button>
112+
</subviews>
113+
<viewLayoutGuide key="safeArea" id="zJ1-ch-MPe"/>
114+
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
115+
</view>
116+
<navigationItem key="navigationItem" id="sHW-hM-6E5"/>
117+
</viewController>
118+
<placeholder placeholderIdentifier="IBFirstResponder" id="3Fx-Ex-bh1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
119+
</objects>
120+
<point key="canvasLocation" x="1054.1984732824426" y="3.5211267605633805"/>
121+
</scene>
122+
<!--Navigation Controller-->
123+
<scene sceneID="0Yh-qJ-qYw">
124+
<objects>
125+
<navigationController storyboardIdentifier="UIKitNavigationRoot" automaticallyAdjustsScrollViewInsets="NO" id="b2m-ne-2Y2" sceneMemberID="viewController">
126+
<toolbarItems/>
127+
<navigationBar key="navigationBar" contentMode="scaleToFill" id="2fV-z7-NIw">
128+
<rect key="frame" x="0.0" y="59" width="393" height="44"/>
129+
<autoresizingMask key="autoresizingMask"/>
130+
</navigationBar>
131+
<nil name="viewControllers"/>
132+
<connections>
133+
<segue destination="bfq-Fn-kGB" kind="relationship" relationship="rootViewController" id="bph-pf-i0m"/>
134+
</connections>
135+
</navigationController>
136+
<placeholder placeholderIdentifier="IBFirstResponder" id="Hr9-Vd-AxC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
137+
</objects>
138+
<point key="canvasLocation" x="127.48091603053435" y="3.5211267605633805"/>
87139
</scene>
88140
</scenes>
89141
<resources>

Examples/SmokeTest/SmokeTestUITests/SmokeTestUITests.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ final class SmokeTestUITests: XCTestCase {
157157
let app = XCUIApplication()
158158
app.launch()
159159
app.buttons["UIKit"].tap()
160-
XCTAssert(app.staticTexts["Sample UIKit App"].waitForExistence(timeout: uiUpdateTimeout))
160+
XCTAssert(app.buttons["Go To UIKit App"].waitForExistence(timeout: uiUpdateTimeout))
161161

162162
app.buttons["Core"].tap()
163163
XCTAssert(app.buttons["Flush"].waitForExistence(timeout: uiUpdateTimeout))
@@ -168,6 +168,9 @@ final class SmokeTestUITests: XCTestCase {
168168
let app = XCUIApplication()
169169
app.launch()
170170
app.buttons["UIKit"].tap()
171+
XCTAssert(app.buttons["Go To UIKit App"].waitForExistence(timeout: uiUpdateTimeout))
172+
app.buttons["Go To UIKit App"].tap()
173+
171174
XCTAssert(app.staticTexts["Sample UIKit App"].waitForExistence(timeout: uiUpdateTimeout))
172175

173176
app.buttons["Simple Button"].tap()

README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,15 @@ UIKit views will automatically be instrumented, emitting `viewDidAppear` and `vi
8686
- `view.title` - Title of the view controller, if provided.
8787
- `view.nibName` - The name of the view controller's nib file, if one was specified.
8888
- `view.animated` - true if the transition to/from this view is animated, false if it isn't.
89-
- `view.class` - name of the swift/objective-c class this view
90-
controller has.
89+
- `view.class` - name of the swift/objective-c class this view controller has.
90+
- `screen.name` - name of the screen that appeared. In order of precedence, this attribute will have the value of the first of these to be set:
91+
- `accessiblityIdentifier` of the view that appeared
92+
- `view.title` - as defined above. If the view is a UINavigationController, Storybook Identifier (below) will be used in preference to `view.title`.
93+
- Storybook Identifier - unique id identifying the view controller within its Storybook.
94+
- `view.class` - as defined above
95+
- `screen.path` - the full path leading to the current view, consisting of the current view's `screen.name` as well as any parent views.
96+
97+
`viewDidAppear` events will also track `screen.name` as the "current screen" (as with the manual instrumentation described below), and will include that value as `screen.name` on other, non-navigation spans emitted.
9198

9299
#### Interaction
93100

Sources/Honeycomb/HoneycombNavigationSpanInstrumentation.swift

+21-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func getTracer() -> Tracer {
1515

1616
internal class HoneycombNavigationProcessor {
1717
static let shared = HoneycombNavigationProcessor()
18-
var currentNavigationPath: String? = nil
18+
var currentNavigationPath: [String] = []
1919

2020
private init() {}
2121

@@ -29,7 +29,7 @@ internal class HoneycombNavigationProcessor {
2929
}
3030

3131
func reportNavigation(path: String) {
32-
currentNavigationPath = path
32+
currentNavigationPath = [path]
3333

3434
// emit a span that says we've navigated to this path
3535
getTracer().spanBuilder(spanName: navigationSpanName)
@@ -71,6 +71,9 @@ internal class HoneycombNavigationProcessor {
7171
reportNavigation(path: unencodablePath)
7272
}
7373

74+
func setCurrentNavigationPath(_ path: [String]) {
75+
currentNavigationPath = path
76+
}
7477
}
7578

7679
extension View {
@@ -96,15 +99,28 @@ public struct HoneycombNavigationPathSpanProcessor: SpanProcessor {
9699
parentContext: SpanContext?,
97100
span: any ReadableSpan
98101
) {
99-
if HoneycombNavigationProcessor.shared.currentNavigationPath != nil {
102+
let currentViewPath = HoneycombNavigationProcessor.shared.currentNavigationPath
103+
if !currentViewPath.isEmpty {
100104
span.setAttribute(
101105
key: "screen.name",
102-
value: HoneycombNavigationProcessor.shared.currentNavigationPath!
106+
value: currentViewPath.last!
107+
)
108+
span.setAttribute(
109+
key: "screen.path",
110+
value: serializePath(currentViewPath)
103111
)
104-
105112
}
106113
}
107114

115+
private func serializePath(_ path: [String]) -> String {
116+
return
117+
path
118+
.filter { str in
119+
!str.starts(with: ("_"))
120+
}
121+
.joined(separator: "/")
122+
}
123+
108124
public func onEnd(span: any ReadableSpan) {}
109125

110126
public func shutdown(explicitTimeout: TimeInterval? = nil) {}

Sources/Honeycomb/UIKit/UIViewController.swift

+30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@
44
import UIKit
55

66
extension UIViewController {
7+
var storyboardId: String? {
8+
return value(forKey: "storyboardIdentifier") as? String
9+
}
10+
11+
private var viewName: String {
12+
// prefer storyboardId over title for UINavigationController
13+
// prefer title over storyboardId for other classes
14+
if self.isKind(of: UINavigationController.self) {
15+
return self.view.accessibilityIdentifier
16+
?? self.storyboardId
17+
?? self.title
18+
?? NSStringFromClass(type(of: self))
19+
}
20+
return self.view.accessibilityIdentifier
21+
?? self.title
22+
?? self.storyboardId
23+
?? NSStringFromClass(type(of: self))
24+
}
25+
26+
private func viewStack() -> [String] {
27+
if var parentPath = self.parent?.viewStack() {
28+
parentPath.append(self.viewName)
29+
return parentPath
30+
}
31+
return [self.viewName]
32+
}
33+
734
private func setAttributes(span: Span, className: String, animated: Bool) {
835
if let title = self.title {
936
span.setAttribute(key: "view.title", value: title)
@@ -20,6 +47,9 @@
2047

2148
// Internal classes from SwiftUI will likely begin with an underscore
2249
if !className.hasPrefix("_") {
50+
// set this _before_ creating the span
51+
HoneycombNavigationProcessor.shared.setCurrentNavigationPath(viewStack())
52+
2353
let span = getUIKitViewTracer().spanBuilder(spanName: "viewDidAppear").startSpan()
2454
setAttributes(span: span, className: className, animated: animated)
2555
span.end()

0 commit comments

Comments
 (0)