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

feat: UIKit Navigation instrimentation #22

Merged
merged 28 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0819b0b
create navigation instrumentation class
arriIsHere Nov 4, 2024
04ce8d9
add first swizzle impl
arriIsHere Nov 5, 2024
e95e6b4
rename and move methods
arriIsHere Nov 7, 2024
96d5078
Move files around and rename to match convention
arriIsHere Nov 7, 2024
43d477f
add storyboard file for UI kit tab
arriIsHere Nov 13, 2024
4a17ec3
Shim for SwiftUI to UIKit
arriIsHere Nov 13, 2024
b7b5d12
Add UIKit tab
arriIsHere Nov 13, 2024
13b93ef
Add test for UIKit
arriIsHere Nov 13, 2024
809b582
fix and add instrumentation
arriIsHere Nov 13, 2024
1cdb040
update storyboard and smoke tests
arriIsHere Nov 15, 2024
aa274e0
fix formatting
arriIsHere Nov 15, 2024
b61fc95
update to pass
arriIsHere Nov 18, 2024
65a8d2a
add disappear test
arriIsHere Nov 18, 2024
35fd328
exactly one line, no count needed
arriIsHere Nov 18, 2024
e11980b
fix method names
arriIsHere Nov 18, 2024
bbd647d
filter out generated SwiftUI classes
arriIsHere Nov 19, 2024
13ef5f3
fix trailing ws
arriIsHere Nov 19, 2024
807feb2
title is only added when present
arriIsHere Nov 19, 2024
ca94603
comment spelling error
arriIsHere Nov 19, 2024
aae1c71
also exclude on disappear
arriIsHere Nov 19, 2024
bdc06a5
rename views
arriIsHere Nov 19, 2024
22b2e9a
Remove duplicate methods and fields
arriIsHere Nov 20, 2024
6967340
update smoke
arriIsHere Nov 20, 2024
a11a82e
use own instrumentation name
arriIsHere Nov 20, 2024
81736a7
test for existing tab view
arriIsHere Nov 20, 2024
48a8ef8
UIKit documentation
arriIsHere Nov 21, 2024
9c18514
add nib name
arriIsHere Nov 22, 2024
8586f74
clean up nits
arriIsHere Nov 22, 2024
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
8 changes: 8 additions & 0 deletions Examples/SmokeTest/SmokeTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
452B71DD2CE52C8600C27FB2 /* ViewInstrumentationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */; };
366309102CE51BDC00B97612 /* UIKitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3663090F2CE51BDC00B97612 /* UIKitView.swift */; };
366309122CE51EF000B97612 /* UIKitView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 366309112CE51EF000B97612 /* UIKitView.storyboard */; };
AF6DEFEA2C8D3CE000363027 /* SmokeTestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6DEFE92C8D3CE000363027 /* SmokeTestApp.swift */; };
AF6DEFEC2C8D3CE000363027 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF6DEFEB2C8D3CE000363027 /* ContentView.swift */; };
AF6DEFEE2C8D3CE100363027 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AF6DEFED2C8D3CE100363027 /* Assets.xcassets */; };
Expand Down Expand Up @@ -39,6 +41,8 @@

/* Begin PBXFileReference section */
452B71DC2CE52C8600C27FB2 /* ViewInstrumentationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInstrumentationView.swift; sourceTree = "<group>"; };
3663090F2CE51BDC00B97612 /* UIKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitView.swift; sourceTree = "<group>"; };
366309112CE51EF000B97612 /* UIKitView.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = UIKitView.storyboard; sourceTree = "<group>"; };
AF6DEFE62C8D3CE000363027 /* SmokeTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SmokeTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
AF6DEFE92C8D3CE000363027 /* SmokeTestApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeTestApp.swift; sourceTree = "<group>"; };
AF6DEFEB2C8D3CE000363027 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -110,6 +114,8 @@
AF6DEFEB2C8D3CE000363027 /* ContentView.swift */,
AF6DEFED2C8D3CE100363027 /* Assets.xcassets */,
AF6DEFEF2C8D3CE100363027 /* Preview Content */,
3663090F2CE51BDC00B97612 /* UIKitView.swift */,
366309112CE51EF000B97612 /* UIKitView.storyboard */,
);
path = SmokeTest;
sourceTree = "<group>";
Expand Down Expand Up @@ -254,6 +260,7 @@
buildActionMask = 2147483647;
files = (
AF6DEFF12C8D3CE100363027 /* Preview Assets.xcassets in Resources */,
366309122CE51EF000B97612 /* UIKitView.storyboard in Resources */,
AF6DEFEE2C8D3CE100363027 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -281,6 +288,7 @@
files = (
AF6DEFEC2C8D3CE000363027 /* ContentView.swift in Sources */,
AFA277C42CC71DF7007F8A46 /* NetworkView.swift in Sources */,
366309102CE51BDC00B97612 /* UIKitView.swift in Sources */,
AF6DEFEA2C8D3CE000363027 /* SmokeTestApp.swift in Sources */,
AFA0BA5C2C8E12E700F54611 /* MetricKitTestHelpers.swift in Sources */,
452B71DD2CE52C8600C27FB2 /* ViewInstrumentationView.swift in Sources */,
Expand Down
9 changes: 9 additions & 0 deletions Examples/SmokeTest/SmokeTest/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ struct ContentView: View {
ViewInstrumentationView()
.padding()
.tabItem { Label("View Instrumentation", systemImage: "ruler") }

UIKitView()
.padding()
.tabItem {
Label(
"UIKit",
systemImage: "paintpalette"
)
}
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions Examples/SmokeTest/SmokeTest/UIKitView.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<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>
<!--View Controller-->
<scene sceneID="s0d-6b-0kx">
<objects>
<viewController storyboardIdentifier="UIKitView" id="Y6W-OH-hqX" customClass="ViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" fixedFrame="YES" image="globe" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="O94-9o-FQ4">
<rect key="frame" x="169" y="216" width="54" height="46"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Sample UIKit App" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="008-k5-M1T">
<rect key="frame" x="130" y="266" width="133" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<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="61.832061068702288" y="3.5211267605633805"/>
</scene>
</scenes>
<resources>
<image name="globe" catalog="system" width="128" height="123"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
35 changes: 35 additions & 0 deletions Examples/SmokeTest/SmokeTest/UIKitView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import SwiftUI
import UIKit

struct UIKitView: View {
var body: some View {
StoryboardViewControllerRepresentation()
}
}

struct UIKView_preview: PreviewProvider {
static var previews: some View {
UIKitView()
}
}

struct StoryboardViewControllerRepresentation: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let storyboard = UIStoryboard(name: "UIKitView", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "UIKitView")
return controller
}

func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}

}
11 changes: 11 additions & 0 deletions Examples/SmokeTest/SmokeTestUITests/SmokeTestUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ final class SmokeTestUITests: XCTestCase {
app.buttons["Flush"].tap()
}

func testUIKitInstrimentation() throws {
let app = XCUIApplication()
app.launch()
app.buttons["UIKit"].tap()
XCTAssert(app.staticTexts["Sample UIKit App"].waitForExistence(timeout: uiUpdateTimeout))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we're swizzling UIViewController, this instrumentation should work for both UINavigationContoller and UITabBarController, among others. Should we add tests for both a navigation controller and a tab view, to make sure they're both reasonable? I'm not sure how annoying that would be to build in Storyboard.


app.buttons["Core"].tap()
XCTAssert(app.buttons["Flush"].waitForExistence(timeout: uiUpdateTimeout))
app.buttons["Flush"].tap()
}

func testRenderPerformace() throws {
let app = XCUIApplication()
app.launch()
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ To manually send a span:
The following auto-instrumentation libraries are automatically included:
* [MetricKit](https://developer.apple.com/documentation/metrickit) data is automatically collected.

### UIKit Instrumentation

UIKit views will automatically be instrumented, emitting `viewDidAppear` and `viewDidDisappear` events. Both have the following attributes:

- `title` - Title of the view, if provided.
- `nibName` - The name of the view controller's nib file, if one was specified.
- `animated` - true if the transition to/from this view is animated, false if it isn't.
- `className` - name of the swift/objective-c class this view
controller has.

## Manual Instrumentation
### SwiftUI View Instrumentation

Expand Down
1 change: 1 addition & 0 deletions Sources/Honeycomb/Honeycomb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ public class Honeycomb {
OpenTelemetry.registerLoggerProvider(loggerProvider: loggerProvider)

installNetworkInstrumentation(options: options)
installUINavigationInstrumentation()

if #available(iOS 13.0, macOS 12.0, *) {
MXMetricManager.shared.add(self.metricKitSubscriber)
Expand Down
18 changes: 18 additions & 0 deletions Sources/Honeycomb/UIKit/NavigationInstrumentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#if canImport(UIKit)
import Foundation
import OpenTelemetryApi
import UIKit

private let honeycombUIKitInstrumentationName = "@honeycombio/instrumentation-uikit"

internal func getUIKitViewTracer() -> Tracer {
return OpenTelemetry.instance.tracerProvider.get(
instrumentationName: honeycombUIKitInstrumentationName,
instrumentationVersion: honeycombLibraryVersion
)
}

public func installUINavigationInstrumentation() {
UIViewController.swizzle()
}
#endif
84 changes: 84 additions & 0 deletions Sources/Honeycomb/UIKit/UIViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#if canImport(UIKit)
import Foundation
import OpenTelemetryApi
import UIKit

extension UIViewController {
@objc func traceViewDidAppear(_ animated: Bool) {
let className = NSStringFromClass(type(of: self))

// Internal classes from SwiftUI will likely begin with an underscore
if !className.hasPrefix("_") {
let span = getUIKitViewTracer().spanBuilder(spanName: "viewDidAppear").startSpan()
if self.title != nil {
span.setAttribute(key: "title", value: self.title!)
}
if self.nibName != nil {
span.setAttribute(key: "nibName", value: self.nibName!)
}
span.setAttribute(key: "animated", value: animated)
span.setAttribute(key: "className", value: className)

span.end()
}

traceViewDidAppear(animated)
}

@objc func traceViewDidDisappear(_ animated: Bool) {

let className = NSStringFromClass(type(of: self))

// Internal classes from SwiftUI will likely begin with an underscore
if !className.hasPrefix("_") {
let span = getUIKitViewTracer().spanBuilder(spanName: "viewDidDisappear")
.startSpan()
if self.title != nil {
span.setAttribute(key: "title", value: self.title!)
}
if self.nibName != nil {
span.setAttribute(key: "nibName", value: self.nibName!)
}
span.setAttribute(key: "animated", value: animated)
span.setAttribute(key: "className", value: className)
span.end()
}

traceViewDidDisappear(animated)
}

public static func swizzle() {
let originalAppearSelector = #selector(UIViewController.viewDidAppear(_:))
let swizzledAppearSelector = #selector(UIViewController.traceViewDidAppear(_:))
let originalDisappearSelector = #selector(UIViewController.viewDidDisappear(_:))
let swizzledDisappearSelector = #selector(UIViewController.traceViewDidDisappear(_:))

guard
let originalAppearMethod = class_getInstanceMethod(self, originalAppearSelector),
let swizzledAppearMethod = class_getInstanceMethod(self, swizzledAppearSelector)
else {
print("unable to swizzle \(originalAppearSelector): original method not found")
return
}

method_exchangeImplementations(originalAppearMethod, swizzledAppearMethod)

guard
let originalDisappearMethod = class_getInstanceMethod(
self,
originalDisappearSelector
),
let swizzledDisappearMethod = class_getInstanceMethod(
self,
swizzledDisappearSelector
)
else {
print("unable to swizzle \(originalDisappearSelector): original method not found")
return
}

method_exchangeImplementations(originalDisappearMethod, swizzledDisappearMethod)
}
}

#endif
20 changes: 20 additions & 0 deletions smoke-tests/smoke-e2e.bats
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,24 @@ mk_diag_attr() {
"main view"
"nested expensive text"
"nested expensive view"'

}

@test "UIViewController attributes are correct" {
result=$(attributes_from_span_named "@honeycombio/instrumentation-uikit" viewDidAppear | \
jq "select (.key == \"className\")" | \
jq "select (.value.stringValue == \"UIViewController\").value.stringValue")
assert_equal "$result" '"UIViewController"'

result=$(attributes_from_span_named "@honeycombio/instrumentation-uikit" viewDidDisappear | \
jq "select (.key == \"className\")" | \
jq "select (.value.stringValue == \"UIViewController\").value.stringValue")
assert_equal "$result" '"UIViewController"'
}

@test "UITabView attributes are correct" {
result=$(attributes_from_span_named "@honeycombio/instrumentation-uikit" viewDidAppear | \
jq "select (.key == \"className\")" | \
jq "select (.value.stringValue == \"SwiftUI.UIKitTabBarController\").value.stringValue" | uniq -c)
assert_equal "$result" ' 5 "SwiftUI.UIKitTabBarController"'
}