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

RUM-3531 WebView slot cache #1760

Merged
merged 3 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 14 additions & 10 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,6 @@
61F2728B25C9561A00D54BF8 /* PLCrashReporterIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2728A25C9561A00D54BF8 /* PLCrashReporterIntegration.swift */; };
61F272B125C95ED800D54BF8 /* Mocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F2729A25C95EB200D54BF8 /* Mocks.swift */; };
61F74AF426F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */; };
61F930CB2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; };
61F930CC2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; };
61F930BE2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */; };
61F930BF2BA1ACAC005F0EE2 /* Storage+TLV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */; };
61F930C22BA1C41A005F0EE2 /* TLVBlockReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */; };
Expand All @@ -491,6 +489,8 @@
61F930C62BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */; };
61F930C82BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */; };
61F930C92BA1C51C005F0EE2 /* Storage+TLVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */; };
61F930CB2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; };
61F930CC2BA213AC005F0EE2 /* AppHang.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F930CA2BA213AC005F0EE2 /* AppHang.swift */; };
61F9CABA2513A7F5000A5E61 /* RUMSessionMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */; };
61FC5F3525CC1898006BB4DE /* CrashContextProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FC5F3425CC1898006BB4DE /* CrashContextProviderTests.swift */; };
61FDBA1326971953001D9D43 /* CrashReportMinifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61FDBA1226971953001D9D43 /* CrashReportMinifier.swift */; };
Expand Down Expand Up @@ -541,6 +541,7 @@
A7EA11622AB0CE6C00C73970 /* DDUIKitRUMActionsPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DA18062AB0CA4700F76337 /* DDUIKitRUMActionsPredicateTests.swift */; };
A7EA88562B17639A00FE2580 /* ResourcesWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7EA88552B17639A00FE2580 /* ResourcesWriter.swift */; };
A7F651302B7655DE004B0EDB /* UIImageResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */; };
D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */; };
D20605A3287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; };
D20605A4287464F40047275C /* ContextValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A2287464F40047275C /* ContextValuePublisher.swift */; };
D20605A6287476230047275C /* ServerOffsetPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20605A5287476230047275C /* ServerOffsetPublisher.swift */; };
Expand Down Expand Up @@ -2410,11 +2411,11 @@
61F3CDA62512144600C816E5 /* UIKitRUMViewsPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRUMViewsPredicate.swift; sourceTree = "<group>"; };
61F3CDAA25121FB500C816E5 /* UIViewControllerSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerSwizzlerTests.swift; sourceTree = "<group>"; };
61F74AF326F20E4600E5F5ED /* DebugCrashReportingWithRUMViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugCrashReportingWithRUMViewController.swift; sourceTree = "<group>"; };
61F930CA2BA213AC005F0EE2 /* AppHang.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHang.swift; sourceTree = "<group>"; };
61F930BD2BA1ACAC005F0EE2 /* Storage+TLV.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+TLV.swift"; sourceTree = "<group>"; };
61F930C12BA1C41A005F0EE2 /* TLVBlockReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlockReader.swift; sourceTree = "<group>"; };
61F930C42BA1C4EB005F0EE2 /* TLVBlockReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLVBlockReaderTests.swift; sourceTree = "<group>"; };
61F930C72BA1C51C005F0EE2 /* Storage+TLVTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+TLVTests.swift"; sourceTree = "<group>"; };
61F930CA2BA213AC005F0EE2 /* AppHang.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHang.swift; sourceTree = "<group>"; };
61F9CA982513977A000A5E61 /* RUMSessionMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMSessionMatcher.swift; sourceTree = "<group>"; };
61FB222C244A21ED00902D19 /* LoggingFeatureMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingFeatureMocks.swift; sourceTree = "<group>"; };
61FB222F244E1BE900902D19 /* DatadogLogsFeatureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogLogsFeatureTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2491,6 +2492,7 @@
B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReaderTests.swift; sourceTree = "<group>"; };
B3FC3C0626526EFF00DEED9E /* VitalInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalInfo.swift; sourceTree = "<group>"; };
B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VitalInfoTests.swift; sourceTree = "<group>"; };
D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireframesBuilderTests.swift; sourceTree = "<group>"; };
D20605A2287464F40047275C /* ContextValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextValuePublisher.swift; sourceTree = "<group>"; };
D20605A5287476230047275C /* ServerOffsetPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOffsetPublisher.swift; sourceTree = "<group>"; };
D20605A82874C1CD0047275C /* NetworkConnectionInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnectionInfoPublisher.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3272,10 +3274,10 @@
61054E242A6EE10A00AAA894 /* ViewTreeSnapshot.swift */,
61054E252A6EE10A00AAA894 /* ViewTreeSnapshotBuilder.swift */,
61054E262A6EE10A00AAA894 /* ViewTreeRecorder.swift */,
61054E272A6EE10A00AAA894 /* NodeRecorders */,
61054E372A6EE10A00AAA894 /* ViewAttributes+Copy.swift */,
61054E382A6EE10A00AAA894 /* ViewTreeRecordingContext.swift */,
61054E392A6EE10A00AAA894 /* NodeIDGenerator.swift */,
61054E272A6EE10A00AAA894 /* NodeRecorders */,
);
path = ViewTreeSnapshot;
sourceTree = "<group>";
Expand Down Expand Up @@ -3352,7 +3354,7 @@
A7B932F42B1F694000AE6477 /* ResourcesProcessor.swift */,
61054E492A6EE10A00AAA894 /* Privacy */,
61054E4C2A6EE10A00AAA894 /* Diffing */,
61054E4F2A6EE10A00AAA894 /* SRDataModelsBuilder */,
61054E4F2A6EE10A00AAA894 /* Builders */,
61054E522A6EE10A00AAA894 /* Flattening */,
);
path = Processor;
Expand All @@ -3375,13 +3377,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 @@ -3452,7 +3454,7 @@
children = (
61054F4F2A6EE1BA00AAA894 /* Privacy */,
61054F512A6EE1BA00AAA894 /* Diffing */,
61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */,
61054F542A6EE1BA00AAA894 /* Builders */,
61054F562A6EE1BA00AAA894 /* SnapshotProcessorTests.swift */,
A7D9528B2B28C18D004C79B1 /* ResourceProcessorTests.swift */,
61054F572A6EE1BA00AAA894 /* Flattening */,
Expand All @@ -3477,12 +3479,13 @@
path = Diffing;
sourceTree = "<group>";
};
61054F542A6EE1BA00AAA894 /* SRDataModelsBuilder */ = {
61054F542A6EE1BA00AAA894 /* Builders */ = {
isa = PBXGroup;
children = (
61054F552A6EE1BA00AAA894 /* RecordsBuilderTests.swift */,
D2056C202BBFE05A0085BC76 /* WireframesBuilderTests.swift */,
);
path = SRDataModelsBuilder;
path = Builders;
sourceTree = "<group>";
};
61054F572A6EE1BA00AAA894 /* Flattening */ = {
Expand Down Expand Up @@ -7880,6 +7883,7 @@
61054FA52A6EE1BA00AAA894 /* RecordsBuilderTests.swift in Sources */,
61054FD02A6EE1BA00AAA894 /* SRContextPublisherTests.swift in Sources */,
61054F9B2A6EE1BA00AAA894 /* QueueTests.swift in Sources */,
D2056C212BBFE05A0085BC76 /* WireframesBuilderTests.swift in Sources */,
61054F992A6EE1BA00AAA894 /* ColorsTests.swift in Sources */,
61054FBF2A6EE1BA00AAA894 /* UIPickerViewRecorderTests.swift in Sources */,
61054FAE2A6EE1BA00AAA894 /* TouchIdentifierGeneratorTests.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 = String(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()
Copy link
Member

Choose a reason for hiding this comment

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

any reason for dropping the fuzzy offset?

Copy link
Member Author

Choose a reason for hiding this comment

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

I have noticed it to be flaky..

)
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,23 @@ public typealias WireframeID = NodeID
/// Note: `WireframesBuilder` is used by `Processor` on a single background thread.
@_spi(Internal)
public class SessionReplayWireframesBuilder {
/// The cache of webview slot IDs in memory during snapshot.
private var webViewSlotIDs: Set<Int>

/// Creates a builder for builder wireframes in snapshot processing.
///
/// The builder takes optional webview slot IDs 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 webviewSlotIDs: The webview slot IDs in memory during snapshot.
init(webViewSlotIDs: Set<Int> = []) {
self.webViewSlotIDs = webViewSlotIDs
}
}

@_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 @@ -177,10 +194,9 @@ public class SessionReplayWireframesBuilder {
return .placeholderWireframe(value: wireframe)
}

public func createWebViewWireframe(
id: Int64,
public func visibleWebViewWireframe(
id: Int,
frame: CGRect,
slotId: String,
clip: SRContentClip? = nil,
borderColor: CGColor? = nil,
borderWidth: CGFloat? = nil,
Expand All @@ -192,17 +208,41 @@ public class SessionReplayWireframesBuilder {
border: createShapeBorder(borderColor: borderColor, borderWidth: borderWidth),
clip: clip,
height: Int64(withNoOverflow: frame.height),
id: id,
id: Int64(id),
isVisible: true,
shapeStyle: createShapeStyle(backgroundColor: backgroundColor, cornerRadius: cornerRadius, opacity: opacity),
slotId: slotId,
slotId: String(id),
width: Int64(withNoOverflow: frame.size.width),
x: Int64(withNoOverflow: frame.minX),
y: Int64(withNoOverflow: frame.minY)
)

/// Remove the slot from the builder because a wireframe
/// has been created.
webViewSlotIDs.remove(id)
return .webviewWireframe(value: wireframe)
}

internal func hiddenWebViewWireframes() -> [SRWireframe] {
defer { webViewSlotIDs.removeAll() }
return webViewSlotIDs.map { id in
let wireframe = SRWebviewWireframe(
border: nil,
clip: nil,
height: 0,
id: Int64(id),
isVisible: false,
shapeStyle: nil,
slotId: String(id),
width: 0,
x: 0,
y: 0
)

return .webviewWireframe(value: wireframe)
}
}

// MARK: - Private

private func createShapeBorder(borderColor: CGColor?, borderWidth: CGFloat?) -> SRShapeBorder? {
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
16 changes: 10 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,16 @@ 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(webViewSlotIDs: viewTreeSnapshot.webViewSlotIDs)
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 wireframes and place them at the beginning
wireframes = builder.hiddenWebViewWireframes() + wireframes

interceptWireframes?(wireframes)

Expand Down
Loading