Skip to content

Commit

Permalink
RUM-3531 WebView slot cache
Browse files Browse the repository at this point in the history
  • Loading branch information
maxep committed Apr 3, 2024
1 parent e25a3bf commit 9eb8ddd
Show file tree
Hide file tree
Showing 21 changed files with 412 additions and 79 deletions.
18 changes: 11 additions & 7 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
D270CDE12B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */; };
D2777D9D29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; };
D2777D9E29F6A75800FFBB40 /* TelemetryReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */; };
D27B8A402BB171BA0031D37E /* WebViewSlotCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B8A3F2BB171BA0031D37E /* WebViewSlotCache.swift */; };
D27D81C12A5D415200281CC2 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; };
D27D81C22A5D415200281CC2 /* DatadogCrashReporting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B7885425C180CB002675B5 /* DatadogCrashReporting.framework */; };
D27D81C32A5D415200281CC2 /* DatadogInternal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23039A5298D513C001A1FA3 /* DatadogInternal.framework */; };
Expand Down Expand Up @@ -2650,6 +2651,7 @@
D270CDDC2B46E3DB0002EACD /* URLSessionDataDelegateSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzler.swift; sourceTree = "<group>"; };
D270CDDF2B46E5A50002EACD /* URLSessionDataDelegateSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataDelegateSwizzlerTests.swift; sourceTree = "<group>"; };
D2777D9C29F6A75800FFBB40 /* TelemetryReceiverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryReceiverTests.swift; sourceTree = "<group>"; };
D27B8A3F2BB171BA0031D37E /* WebViewSlotCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSlotCache.swift; sourceTree = "<group>"; };
D286626D2A43487500852CE3 /* Datadog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Datadog.swift; sourceTree = "<group>"; };
D28F836729C9E71C00EF8EA2 /* DDSpanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDSpanTests.swift; sourceTree = "<group>"; };
D28F836A29C9E7A300EF8EA2 /* TracingURLSessionHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingURLSessionHandlerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3269,10 +3271,11 @@
61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */,
61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */,
61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */,
61054E272A6EE10A00AAA894 /* NodeRecorders */,
61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */,
61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */,
61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */,
D27B8A3F2BB171BA0031D37E /* WebViewSlotCache.swift */,
61054E272A6EE10A00AAA894 /* NodeRecorders */,
);
path = ViewTreeSnapshot;
sourceTree = "<group>";
Expand Down Expand Up @@ -3349,7 +3352,7 @@
A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */,
61054E492A6EE10A00AAA894 /* Privacy */,
61054E4C2A6EE10A00AAA894 /* Diffing */,
61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */,
61054E4F2A6EE10A00AAA894 /* Builders */,
61054E522A6EE10A00AAA894 /* Flattening */,
);
path = Processor;
Expand All @@ -3372,13 +3375,13 @@
path = Diffing;
sourceTree = "<group>";
};
61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */ = {
61054E4F2A6EE10A00AAA894 /* Builders */ = {
isa = PBXGroup;
children = (
61054E502A6EE10A00AAA894 /* RecordsBuilder.swift */,
61054E512A6EE10A00AAA894 /* WireframesBuilder.swift */,
);
path = SRDataModelsBuilder;
path = Builders;
sourceTree = "<group>";
};
61054E522A6EE10A00AAA894 /* Flattening */ = {
Expand Down Expand Up @@ -3449,7 +3452,7 @@
children = (
61054F4F2A6EE1BA00AAA894 /* Privacy */,
61054F512A6EE1BA00AAA894 /* Diffing */,
61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */,
61054F542A6EE1BA00AAA894 /* Builders */,
61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */,
A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */,
61054F572A6EE1BA00AAA894 /* Flattening */,
Expand All @@ -3474,12 +3477,12 @@
path = Diffing;
sourceTree = "<group>";
};
61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */ = {
61054F542A6EE1BA00AAA894 /* Builders */ = {
isa = PBXGroup;
children = (
61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */,
);
path = SRDataModelsBuilder;
path = Builders;
sourceTree = "<group>";
};
61054F572A6EE1BA00AAA894 /* Flattening */ = {
Expand Down Expand Up @@ -7757,6 +7760,7 @@
61054E962A6EE10A00AAA894 /* Diff+SRWireframes.swift in Sources */,
D22C5BD02A98A6660024CC1F /* Baggages.swift in Sources */,
61054E902A6EE10A00AAA894 /* SegmentJSON.swift in Sources */,
D27B8A402BB171BA0031D37E /* WebViewSlotCache.swift in Sources */,
61054E672A6EE10A00AAA894 /* Recorder.swift in Sources */,
61054E6B2A6EE10A00AAA894 /* CFType+Safety.swift in Sources */,
61054E852A6EE10A00AAA894 /* UISegmentRecorder.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class WebRecordIntegrationTests: XCTestCase {

func testWebRecordIntegration() throws {
// Given
let webView = WKWebView()
let randomApplicationID: String = .mockRandom()
let randomUUID: UUID = .mockRandom()
let randomBrowserViewID: UUID = .mockRandom()
Expand All @@ -65,7 +66,7 @@ class WebRecordIntegrationTests: XCTestCase {

// When
RUMMonitor.shared(in: core).startView(key: "web-view")
controller.send(body: body)
controller.send(body: body, from: webView)
controller.flush()

// Then
Expand All @@ -74,7 +75,7 @@ class WebRecordIntegrationTests: XCTestCase {
let segment = try XCTUnwrap(segments.first)

let expectedUUID = randomUUID.uuidString.lowercased()
let expectedSlotID = "\(controller.hash as Int)" // explicitly get the NSObject.hash
let expectedSlotID = "\(webView.hash)"

XCTAssertEqual(segment.applicationID, randomApplicationID)
XCTAssertEqual(segment.sessionID, expectedUUID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ final class WKUserContentControllerMock: WKUserContentController {
handlers[name] = nil
}

func send(body: Any) {
func send(body: Any, from webView: WKWebView? = nil) {
let handler = handlers[DDScriptMessageHandler.name]
let message = WKScriptMessageMock(body: body, name: DDScriptMessageHandler.name)
let message = WKScriptMessageMock(body: body, name: DDScriptMessageHandler.name, webView: webView)
handler?.userContentController(self, didReceive: message)
}

Expand All @@ -41,14 +41,17 @@ final class WKUserContentControllerMock: WKUserContentController {
private final class WKScriptMessageMock: WKScriptMessage {
private let _body: Any
private let _name: String
private weak var _webView: WKWebView?

init(body: Any, name: String) {
init(body: Any, name: String, webView: WKWebView? = nil) {
_body = body
_name = name
_webView = webView
}

override var body: Any { _body }
override var name: String { _name }
override weak var webView: WKWebView? { _webView }
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,7 @@ class WebViewEventReceiverTests: XCTestCase {

func testGivenRUMContextAvailable_whenReceivingWebEvent_itGetsEnrichedWithOtherMobileContextAndWritten() throws {
let core = PassthroughCoreMock(
context: .mockWith(
source: "react-native",
serverTimeOffset: .mockRandom(min: -10, max: 10).rounded()
)
context: .mockWith(source: "react-native")
)

// Given
Expand Down
10 changes: 9 additions & 1 deletion DatadogSessionReplay/Sources/Models/SRDataModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,9 @@ public struct SRWebviewWireframe: Codable, Hashable {
/// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime.
public let id: Int64

/// Whether this web-view is visible or not.
public let isVisible: Bool?

/// The style of this wireframe.
public let shapeStyle: SRShapeStyle?

Expand All @@ -475,6 +478,7 @@ public struct SRWebviewWireframe: Codable, Hashable {
case clip = "clip"
case height = "height"
case id = "id"
case isVisible = "isVisible"
case shapeStyle = "shapeStyle"
case slotId = "slotId"
case type = "type"
Expand Down Expand Up @@ -970,6 +974,9 @@ public struct SRIncrementalSnapshotRecord: Codable {
/// Defines the unique ID of the wireframe. This is persistent throughout the view lifetime.
public let id: Int64

/// Whether this web-view is visible or not.
public let isVisible: Bool?

/// The style of this wireframe.
public let shapeStyle: SRShapeStyle?

Expand All @@ -993,6 +1000,7 @@ public struct SRIncrementalSnapshotRecord: Codable {
case clip = "clip"
case height = "height"
case id = "id"
case isVisible = "isVisible"
case shapeStyle = "shapeStyle"
case slotId = "slotId"
case type = "type"
Expand Down Expand Up @@ -1322,4 +1330,4 @@ public enum SRRecord: Codable {
}
}
#endif
// Generated from https://github.com/DataDog/rum-events-format/tree/c3747b3facf75e51cbad4c32f77ec3894f5a7249
// Generated from https://github.com/DataDog/rum-events-format/tree/be033e3251da4a20891a774f9843c489a693c80d
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ public typealias WireframeID = NodeID
/// Note: `WireframesBuilder` is used by `Processor` on a single background thread.
@_spi(Internal)
public class SessionReplayWireframesBuilder {
/// The cache of webview slots in memory during snapshot.
private(set) var webviews: [Int: WebViewSlot]

/// Creates a builder for builder wireframes in snapshot processing.
///
/// The builder takes optional webview slots in cache that can be updated
/// while traversing the node. The cache will be used to create wireframes
/// that are not visible be still need to be kept by the player.
///
/// - Parameter webviews: The webview slot cache.
init(webviews: [Int: WebViewSlot] = [:]) {
self.webviews = webviews
}

/// Removes a webview slot.
///
/// Any node builder should remove the slot that is visible so it can be
/// placed at the right index in the wireframe list. Any remaining slot in
/// cache are considered hidden.
///
/// - Parameter id: The id of the slot to remove.
func removeWebView(withSlotID id: Int) {
webviews[id] = nil
}
}

@_spi(Internal)
extension SessionReplayWireframesBuilder {
/// A set of fallback values to use if the actual value cannot be read or converted.
///
/// The idea is to always provide value, which would make certain element visible in the player.
Expand Down Expand Up @@ -186,13 +214,15 @@ public class SessionReplayWireframesBuilder {
borderWidth: CGFloat? = nil,
backgroundColor: CGColor? = nil,
cornerRadius: CGFloat? = nil,
opacity: CGFloat? = nil
opacity: CGFloat? = nil,
isVisible: Bool? = nil
) -> SRWireframe {
let wireframe = SRWebviewWireframe(
border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth),
clip: clip,
height: Int64(withNoOverflow: frame.height),
id: id,
isVisible: isVisible,
shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity),
slotId: slotId,
width: Int64(withNoOverflow: frame.size.width),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ extension SRWebviewWireframe: MutableWireframe {
clip: use(clip, ifDifferentThan: other.clip),
height: use(height, ifDifferentThan: other.height),
id: id,
isVisible: use(isVisible, ifDifferentThan: other.isVisible),
shapeStyle: use(shapeStyle, ifDifferentThan: other.shapeStyle),
slotId: slotId,
width: use(width, ifDifferentThan: other.width),
Expand Down
18 changes: 12 additions & 6 deletions DatadogSessionReplay/Sources/Processor/SnapshotProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ internal protocol SnapshotProcessing {
internal class SnapshotProcessor: SnapshotProcessing {
/// Flattens VTS received from `Recorder` by removing invisible nodes.
private let nodesFlattener = NodesFlattener()
/// Builds SR wireframes to describe UI elements.
private let wireframesBuilder = WireframesBuilder()
/// Builds SR records to transport SR wireframes.
private let recordsBuilder: RecordsBuilder

Expand Down Expand Up @@ -78,10 +76,18 @@ internal class SnapshotProcessor: SnapshotProcessing {
}

private func processSync(viewTreeSnapshot: ViewTreeSnapshot, touchSnapshot: TouchSnapshot?) {
let flattenedNodes = nodesFlattener.flattenNodes(in: viewTreeSnapshot)
let wireframes: [SRWireframe] = flattenedNodes
.map { node in node.wireframesBuilder }
.flatMap { nodeBuilder in nodeBuilder.buildWireframes(with: wireframesBuilder) }
let builder = WireframesBuilder(webviews: viewTreeSnapshot.webviews)
let nodes = nodesFlattener.flattenNodes(in: viewTreeSnapshot)

// build wireframe from nodes
var wireframes: [SRWireframe] = nodes.flatMap { node in
node.wireframesBuilder.buildWireframes(with: builder)
}

// build hidden webview wireframe and place them at the beginning
wireframes = builder.webviews.values.flatMap { webview in
webview.hiddenWireframes(with: builder)
} + wireframes

interceptWireframes?(wireframes)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,83 @@ internal class WKWebViewRecorder: NodeRecorder {
let identifier = UUID()

func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? {
guard let webView = view as? WKWebView else {
guard let webview = view as? WKWebView else {
return nil
}

guard attributes.isVisible else {
return InvisibleElement.constant
}

let builder = WKWebViewWireframesBuilder(
wireframeID: context.ids.nodeID(view: view, nodeRecorder: self),
slotID: webView.configuration.userContentController.hash,
attributes: attributes
)
let slot = WKWebViewSlot(webview: webview)
// Add or update the webview slot in cache
context.webviewCache.update(slot)

let builder = WKWebViewWireframesBuilder(slot: slot, attributes: attributes)
let node = Node(viewAttributes: attributes, wireframesBuilder: builder)
return SpecificElement(subtreeStrategy: .ignore, nodes: [node])
}
}

/// The slot recorded for a `WKWebView`.
internal struct WKWebViewSlot: WebViewSlot {
/// Weak reference to the web view represented by this slot.
///
/// If the webview become `nil`, the slot will diseappear at the
/// next recording cycle during `reset`
weak var webview: WKWebView?

/// The slot id.
let id: Int

init(webview: WKWebView) {
self.webview = webview
self.id = webview.hash
}

func reset() -> WebViewSlot? {
webview == nil ? nil : self
}

func hiddenWireframes(with builder: SessionReplayWireframesBuilder) -> [SRWireframe] {
return [
builder.createWebViewWireframe(
id: Int64(id),
frame: .zero,
slotId: String(id),
isVisible: false
)
]
}
}

internal struct WKWebViewWireframesBuilder: NodeWireframesBuilder {
let wireframeID: WireframeID
/// The slot identifier of the webview controller.
let slotID: Int
/// Attributes of the `UIView`.
/// The webview slot.
let slot: WebViewSlot

let attributes: ViewAttributes

var wireframeRect: CGRect {
attributes.frame
}
var wireframeRect: CGRect { attributes.frame }

func buildWireframes(with builder: WireframesBuilder) -> [SRWireframe] {
guard attributes.isVisible else {
// ignore hidden webview, the wireframes will be built
// by the slot itself
return []
}

/// Remove the slot from the builder because it has an associated node
builder.removeWebView(withSlotID: slot.id)
return [
builder.createWebViewWireframe(
id: wireframeID,
frame: wireframeRect,
slotId: String(slotID),
id: Int64(slot.id),
frame: attributes.frame,
slotId: String(slot.id),
borderColor: attributes.layerBorderColor,
borderWidth: attributes.layerBorderWidth,
backgroundColor: attributes.backgroundColor,
cornerRadius: attributes.layerCornerRadius,
opacity: attributes.alpha
opacity: attributes.alpha,
isVisible: true
)
]
}
}

#endif
Loading

0 comments on commit 9eb8ddd

Please sign in to comment.