From 7e6da3bafb431a4d56451f3d557a31fdcb5f0eff Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Mon, 6 Nov 2023 16:35:43 +0100 Subject: [PATCH 01/11] Add first implementation of recorder --- .../ios/Sources/DdSessionReplay.mm | 3 +- .../DdSessionReplayImplementation.swift | 26 ++-- .../ios/Sources/RCTTextViewRecorder.swift | 130 ++++++++++++++++++ 3 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm index 9f3a79b34..a416d5413 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm @@ -14,6 +14,7 @@ @implementation DdSessionReplay +@synthesize bridge = _bridge; RCT_EXPORT_MODULE() RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate @@ -36,7 +37,7 @@ @implementation DdSessionReplay - (DdSessionReplayImplementation*)ddSessionReplayImplementation { if (_ddSessionReplayImplementation == nil) { - _ddSessionReplayImplementation = [[DdSessionReplayImplementation alloc] init]; + _ddSessionReplayImplementation = [[DdSessionReplayImplementation alloc] initWithBridge:_bridge]; } return _ddSessionReplayImplementation; } diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift index 61515b9b7..8f0c92411 100644 --- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift +++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift @@ -5,30 +5,40 @@ */ import Foundation -import DatadogSessionReplay +@_spi(Internal) import DatadogSessionReplay import DatadogInternal +import React @objc public class DdSessionReplayImplementation: NSObject { private lazy var sessionReplay: SessionReplayProtocol = sessionReplayProvider() private let sessionReplayProvider: () -> SessionReplayProtocol + private let uiManager: RCTUIManager - internal init(_ sessionReplayProvider: @escaping () -> SessionReplayProtocol) { + internal init(sessionReplayProvider: @escaping () -> SessionReplayProtocol, uiManager: RCTUIManager) { self.sessionReplayProvider = sessionReplayProvider + self.uiManager = uiManager } @objc - public override convenience init() { - self.init({ NativeSessionReplay() }) + public convenience init(bridge: RCTBridge) { + self.init( + sessionReplayProvider: { NativeSessionReplay() }, + uiManager: bridge.uiManager + ) } @objc public func enable(replaySampleRate: Double, defaultPrivacyLevel: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void { + var sessionReplayConfiguration = SessionReplay.Configuration( + replaySampleRate: Float(replaySampleRate), + defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString) + ) + + sessionReplayConfiguration.setAdditionalNodeRecorders([RCTTextViewRecorder(uiManager: self.uiManager)]) + sessionReplay.enable( - with: SessionReplay.Configuration( - replaySampleRate: Float(replaySampleRate), - defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString) - ) + with: sessionReplayConfiguration ) resolve(nil) } diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift new file mode 100644 index 000000000..5dd23b8aa --- /dev/null +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -0,0 +1,130 @@ +/* + * 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-Present Datadog, Inc. + */ + +import UIKit +@_spi(Internal) import DatadogSessionReplay +import React + +// var textObfuscator: (ViewTreeRecordingContext, _ isSensitive: Bool) -> TextObfuscating = { context, isSensitive in +// if isSensitive { +// return context.recorder.privacy.sensitiveTextObfuscator +// } +// +// return context.recorder.privacy.staticTextObfuscator +// } + +@_spi(Internal) public class RCTTextViewRecorder: SessionReplayNodeRecorder { + public var identifier = UUID() + + public let uiManager: RCTUIManager + + public init(uiManager: RCTUIManager) { + self.uiManager = uiManager + } + + public func semantics( + of view: UIView, + with attributes: DatadogSessionReplay.SessionReplayViewAttributes, + in context: DatadogSessionReplay.SessionReplayViewTreeRecordingContext + ) -> DatadogSessionReplay.SessionReplayNodeSemantics? { + guard let textView = view as? RCTTextView else { + return nil + } + + var shadowView: RCTTextShadowView? = nil + let tag = textView.reactTag + + RCTGetUIManagerQueue().sync { + shadowView = uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView + } + + if let shadow = shadowView { + let builder = RCTTextViewWireframesBuilder( + wireframeID: context.ids.nodeID(view: textView, nodeRecorder: self), + attributes: attributes, + // This relies on a change on RN to expose the textStorage. + // We could rely on textView.accessibilityLabel or check what else we could get + text: textView.accessibilityLabel ?? "", + textAlignment: shadow.textAttributes.alignment, + textColor: shadow.textAttributes.foregroundColor?.cgColor, + // check this works + font: shadow.textAttributes.effectiveFont(), +// textObfuscator: textObfuscator(context, false), + // this is currently incorrect + contentRect: shadow.contentFrame + ) + + let node = SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder) + return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [node]) + } + + return SessionReplayInvisibleElement.constant + } +} + +@_spi(Internal) public struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder { + let wireframeID: WireframeID + /// Attributes of the base `UIView`. + let attributes: SessionReplayViewAttributes + /// The text inside text field. + let text: String + /// The alignment of the text. + var textAlignment: NSTextAlignment + /// The color of the text. + let textColor: CGColor? + /// The font used by the text field. + let font: UIFont? + /// Text obfuscator for masking text. +// let textObfuscator: TextObfuscating + /// The frame of the text content + let contentRect: CGRect + + public var wireframeRect: CGRect { + attributes.frame + } + + private var clip: ContentClip { + let top = abs(contentRect.origin.y) + let left = abs(contentRect.origin.x) + let bottom = max(contentRect.height - attributes.frame.height - top, 0) + let right = max(contentRect.width - attributes.frame.width - left, 0) + return ContentClip( + bottom: Int64(withNoOverflow: bottom), + left: Int64(withNoOverflow: left), + right: Int64(withNoOverflow: right), + top: Int64(withNoOverflow: top) + ) + } + + private var relativeIntersectedRect: CGRect { + return CGRect( + x: attributes.frame.origin.x - contentRect.origin.x, + y: attributes.frame.origin.y - contentRect.origin.y , + width: max(contentRect.width, attributes.frame.width), + height: max(contentRect.height, attributes.frame.height) + ) + } + + public func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [Wireframe] { + return [ + builder.createTextWireframe( + id: wireframeID, + frame: relativeIntersectedRect, +// text: textObfuscator.mask(text: text), + text: text, + textAlignment: .init(systemTextAlignment: textAlignment, vertical: .top), + clip: clip, + textColor: textColor, + font: font, + borderColor: attributes.layerBorderColor, + borderWidth: attributes.layerBorderWidth, + backgroundColor: attributes.backgroundColor, + cornerRadius: attributes.layerCornerRadius, + opacity: attributes.alpha + ) + ] + } +} From e70718cf5d95ebdcba07f6bfe6b80f2f03f0494f Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Wed, 15 Nov 2023 17:38:44 +0100 Subject: [PATCH 02/11] Extract text from subviews on main thread --- .../ios/Sources/RCTTextViewRecorder.swift | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 5dd23b8aa..672eb2d8b 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -5,18 +5,11 @@ */ import UIKit -@_spi(Internal) import DatadogSessionReplay +@_spi(Internal) +import DatadogSessionReplay import React -// var textObfuscator: (ViewTreeRecordingContext, _ isSensitive: Bool) -> TextObfuscating = { context, isSensitive in -// if isSensitive { -// return context.recorder.privacy.sensitiveTextObfuscator -// } -// -// return context.recorder.privacy.staticTextObfuscator -// } - -@_spi(Internal) public class RCTTextViewRecorder: SessionReplayNodeRecorder { +internal class RCTTextViewRecorder: SessionReplayNodeRecorder { public var identifier = UUID() public let uiManager: RCTUIManager @@ -24,6 +17,24 @@ import React public init(uiManager: RCTUIManager) { self.uiManager = uiManager } + + internal func extractTextFromSubViews( + subviews: [RCTShadowView]? + ) -> String? { + if let subviews = subviews { + return subviews.compactMap { subview in + if let sub = subview as? RCTRawTextShadowView { + return sub.text + } + if let sub = subview as? RCTVirtualTextShadowView { + // We recursively get all subviews for nested Text components + return extractTextFromSubViews(subviews: sub.reactSubviews()) + } + return nil + }.joined() + } + return nil + } public func semantics( of view: UIView, @@ -42,56 +53,54 @@ import React } if let shadow = shadowView { + // TODO: RUM-2173 check performance is ok + let text = extractTextFromSubViews( + subviews: shadow.reactSubviews() + ) + let builder = RCTTextViewWireframesBuilder( wireframeID: context.ids.nodeID(view: textView, nodeRecorder: self), attributes: attributes, - // This relies on a change on RN to expose the textStorage. - // We could rely on textView.accessibilityLabel or check what else we could get - text: textView.accessibilityLabel ?? "", + text: text, textAlignment: shadow.textAttributes.alignment, textColor: shadow.textAttributes.foregroundColor?.cgColor, - // check this works - font: shadow.textAttributes.effectiveFont(), -// textObfuscator: textObfuscator(context, false), - // this is currently incorrect + font: shadow.textAttributes.effectiveFont(), // Custom fonts are currently not supported for iOS contentRect: shadow.contentFrame ) - let node = SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder) - return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [node]) + return SessionReplaySpecificElement(subtreeStrategy: .record, nodes: [node]) } - return SessionReplayInvisibleElement.constant } } -@_spi(Internal) public struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder { +internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder { let wireframeID: WireframeID /// Attributes of the base `UIView`. let attributes: SessionReplayViewAttributes - /// The text inside text field. - let text: String + /// The text + let text: String? /// The alignment of the text. var textAlignment: NSTextAlignment /// The color of the text. let textColor: CGColor? /// The font used by the text field. let font: UIFont? - /// Text obfuscator for masking text. -// let textObfuscator: TextObfuscating /// The frame of the text content let contentRect: CGRect public var wireframeRect: CGRect { attributes.frame } - - private var clip: ContentClip { + + let DEFAULT_FONT_COLOR = UIColor.black.cgColor + + private var clip: SRContentClip { let top = abs(contentRect.origin.y) let left = abs(contentRect.origin.x) let bottom = max(contentRect.height - attributes.frame.height - top, 0) let right = max(contentRect.width - attributes.frame.width - left, 0) - return ContentClip( + return SRContentClip.create( bottom: Int64(withNoOverflow: bottom), left: Int64(withNoOverflow: left), right: Int64(withNoOverflow: right), @@ -108,16 +117,15 @@ import React ) } - public func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [Wireframe] { + public func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [SRWireframe] { return [ builder.createTextWireframe( id: wireframeID, frame: relativeIntersectedRect, -// text: textObfuscator.mask(text: text), - text: text, - textAlignment: .init(systemTextAlignment: textAlignment, vertical: .top), + text: text ?? "", + textAlignment: .init(systemTextAlignment: textAlignment, vertical: .center), clip: clip, - textColor: textColor, + textColor: textColor ?? DEFAULT_FONT_COLOR, font: font, borderColor: attributes.layerBorderColor, borderWidth: attributes.layerBorderWidth, From 549677a9412825120a7f8aa4385772552d7de697 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Wed, 15 Nov 2023 17:49:49 +0100 Subject: [PATCH 03/11] Obfuscate text content according to privacy level --- .../ios/Sources/RCTTextViewRecorder.swift | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 672eb2d8b..474cf217d 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -10,14 +10,18 @@ import DatadogSessionReplay import React internal class RCTTextViewRecorder: SessionReplayNodeRecorder { - public var identifier = UUID() - - public let uiManager: RCTUIManager - - public init(uiManager: RCTUIManager) { + internal var textObfuscator: (SessionReplayViewTreeRecordingContext) -> SessionReplayTextObfuscating = { context in + return context.recorder.privacy.staticTextObfuscator + } + + internal var identifier = UUID() + + internal let uiManager: RCTUIManager + + internal init(uiManager: RCTUIManager) { self.uiManager = uiManager } - + internal func extractTextFromSubViews( subviews: [RCTShadowView]? ) -> String? { @@ -44,10 +48,10 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { guard let textView = view as? RCTTextView else { return nil } - + var shadowView: RCTTextShadowView? = nil let tag = textView.reactTag - + RCTGetUIManagerQueue().sync { shadowView = uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView } @@ -64,6 +68,7 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { text: text, textAlignment: shadow.textAttributes.alignment, textColor: shadow.textAttributes.foregroundColor?.cgColor, + textObfuscator: textObfuscator(context), font: shadow.textAttributes.effectiveFont(), // Custom fonts are currently not supported for iOS contentRect: shadow.contentFrame ) @@ -78,12 +83,14 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder let wireframeID: WireframeID /// Attributes of the base `UIView`. let attributes: SessionReplayViewAttributes - /// The text + /// The text. let text: String? /// The alignment of the text. var textAlignment: NSTextAlignment /// The color of the text. let textColor: CGColor? + /// The text obfuscator. + let textObfuscator: SessionReplayTextObfuscating /// The font used by the text field. let font: UIFont? /// The frame of the text content @@ -122,7 +129,7 @@ internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder builder.createTextWireframe( id: wireframeID, frame: relativeIntersectedRect, - text: text ?? "", + text: textObfuscator.mask(text: text ?? ""), textAlignment: .init(systemTextAlignment: textAlignment, vertical: .center), clip: clip, textColor: textColor ?? DEFAULT_FONT_COLOR, From ca94badb28444b29a97db08b122cf3c73d5eabc7 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Wed, 15 Nov 2023 18:02:36 +0100 Subject: [PATCH 04/11] Fix tests compilation --- .../ios/Tests/DdSessionReplayTests.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift index 0af63ffaf..7dbc93d15 100644 --- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift +++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import DatadogSDKReactNativeSessionReplay import DatadogSessionReplay +import React internal class DdSessionReplayTests: XCTestCase { private func mockResolve(args: Any?) {} @@ -14,7 +15,8 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithZeroReplaySampleRate() { let sessionReplayMock = MockSessionReplay() - DdSessionReplayImplementation({ sessionReplayMock }) + let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) .enable(replaySampleRate: 0, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 0.0, privacyLevel: .mask)) @@ -22,7 +24,8 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithMaskPrivacyLevel() { let sessionReplayMock = MockSessionReplay() - DdSessionReplayImplementation({ sessionReplayMock }) + let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask)) @@ -30,7 +33,8 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithMaskUserInputPrivacyLevel() { let sessionReplayMock = MockSessionReplay() - DdSessionReplayImplementation({ sessionReplayMock }) + let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK_USER_INPUT", resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .maskUserInput)) @@ -38,7 +42,8 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithAllowPrivacyLevel() { let sessionReplayMock = MockSessionReplay() - DdSessionReplayImplementation({ sessionReplayMock }) + let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) .enable(replaySampleRate: 100, defaultPrivacyLevel: "ALLOW", resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .allow)) @@ -46,7 +51,8 @@ internal class DdSessionReplayTests: XCTestCase { func testEnablesSessionReplayWithBadPrivacyLevel() { let sessionReplayMock = MockSessionReplay() - DdSessionReplayImplementation({ sessionReplayMock }) + let uiManagerMock = MockUIManager() + DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock) .enable(replaySampleRate: 100, defaultPrivacyLevel: "BAD_VALUE", resolve: mockResolve, reject: mockReject) XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask)) @@ -69,3 +75,5 @@ private class MockSessionReplay: SessionReplayProtocol { ) } } + +private class MockUIManager: RCTUIManager {} From 99fb15df34b2471daf4048a3a0855a77eacd0d57 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:25:35 +0100 Subject: [PATCH 05/11] Add test for RCTTextView recorder --- .../ios/Tests/RCTTextViewRecorderTests.swift | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift diff --git a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift new file mode 100644 index 000000000..852fde475 --- /dev/null +++ b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift @@ -0,0 +1,182 @@ +/* + * 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 XCTest +@testable import DatadogSDKReactNativeSessionReplay +@_spi(Internal) +@testable import DatadogSessionReplay +import React + +internal class RCTTextViewRecorderTests: XCTestCase { + let mockAttributes = SessionReplayViewAttributes( + frame: CGRect(x: 0, y: 0, width: 100, height: 100), + backgroundColor: UIColor.white.cgColor, + layerBorderColor: UIColor.blue.cgColor, + layerBorderWidth: CGFloat(1.0), + layerCornerRadius: CGFloat(1.0), + alpha: CGFloat(1.0), + isHidden: false, + intrinsicContentSize: CGSize(width: 100.0, height: 100.0) + ) + + let mockAllowContext = SessionReplayViewTreeRecordingContext( + recorder: .init(privacy: SessionReplayPrivacyLevel.allow, applicationID: "app_id", sessionID: "session_id", viewID: "view_id", viewServerTimeOffset: nil), + coordinateSpace: UIView(), + ids: .init(), + imageDataProvider: ImageDataProvider() + ) + + var mockShadowView: RCTTextShadowView { + // The shadow view must be initialized with a bridge so that we can insert React Subviews into it. + let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none)); + + let rawTextShadowView = RCTRawTextShadowView() + rawTextShadowView.text = "This is the test text." + shadowView.insertReactSubview(rawTextShadowView, at: 0) + + return shadowView + } + + var mockShadowViewNestedText: RCTTextShadowView { + // The shadow view must be initialized with a bridge so that we can insert React Subviews into it. + let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none)); + + let rawTextShadowView = RCTRawTextShadowView() + rawTextShadowView.text = "This is the " + shadowView.insertReactSubview(rawTextShadowView, at: 0) + + let virtualTextShadowView = RCTVirtualTextShadowView() + let nestedRawTextShadowView = RCTRawTextShadowView() + nestedRawTextShadowView.text = "nested test text." + virtualTextShadowView.insertReactSubview(nestedRawTextShadowView, at: 0) + shadowView.insertReactSubview(virtualTextShadowView, at: 1) + + return shadowView + } + + func testReturnsNilIfViewIsNotRCTTextView() { + let viewMock = UIView() + let uiManagerMock = MockUIManager() + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + + let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) + + XCTAssertNil(result) + } + + func testReturnsInvisibleElementIfShadowViewIsNotFound() throws { + let reactTag = NSNumber(value: 44) + let uiManagerMock = MockUIManager() + let viewMock = RCTTextView() + viewMock.reactTag = reactTag + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + + let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) + + let element = try XCTUnwrap(result as? SessionReplayInvisibleElement) + XCTAssertEqual(element, SessionReplayInvisibleElement.constant) + } + + func testReturnsBuilderWithCorrectInformation() throws { + let reactTag = NSNumber(value: 44) + let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView) + let viewMock = RCTTextView() + viewMock.reactTag = reactTag + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + + let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) + + let element = try XCTUnwrap(result as? SessionReplaySpecificElement) + XCTAssertEqual(element.subtreeStrategy, .ignore) + XCTAssertEqual(element.nodes.count, 1) + let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe()) + XCTAssertEqual(wireframe.text, "This is the test text.") + } + + func testReturnsBuilderWithCorrectInformationWhenNestedTextComponents() throws { + let reactTag = NSNumber(value: 44) + let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowViewNestedText) + let viewMock = RCTTextView() + viewMock.reactTag = reactTag + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + + let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext) + + let element = try XCTUnwrap(result as? SessionReplaySpecificElement) + XCTAssertEqual(element.subtreeStrategy, .ignore) + XCTAssertEqual(element.nodes.count, 1) + let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe()) + XCTAssertEqual(wireframe.text, "This is the nested test text.") + } + + func testReturnsBuilderWithCorrectInformationWhenTextIsObfuscated() throws { + let mockMaskContext = SessionReplayViewTreeRecordingContext( + recorder: .init(privacy: SessionReplayPrivacyLevel.mask, applicationID: "app_id", sessionID: "session_id", viewID: "view_id", viewServerTimeOffset: nil), + coordinateSpace: UIView(), + ids: .init(), + imageDataProvider: ImageDataProvider() + ) + let reactTag = NSNumber(value: 44) + let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView) + let viewMock = RCTTextView() + viewMock.reactTag = reactTag + let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock) + + let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockMaskContext) + + let element = try XCTUnwrap(result as? SessionReplaySpecificElement) + XCTAssertEqual(element.subtreeStrategy, .ignore) + XCTAssertEqual(element.nodes.count, 1) + let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe()) + XCTAssertEqual(wireframe.text, "xxxx xx xxx xxxx xxxxx") + } +} + +private class MockRCTTextView: RCTTextView {} + +private class MockUIManager: RCTUIManager { + /// Tag to be used in the test corresponding to a shadow view + var shadowViewTag: NSNumber? = nil + var shadowView: RCTTextShadowView? = nil + + convenience init(reactTag: NSNumber, shadowView: RCTTextShadowView?) { + self.init() + self.shadowViewTag = reactTag + self.shadowView = shadowView + } + + internal override func shadowView(forReactTag: NSNumber) -> RCTShadowView? { + if (forReactTag == shadowViewTag) { + return shadowView + } + return nil + } + +} + +extension SessionReplayInvisibleElement: Equatable { + public static func ==(lhs: SessionReplayInvisibleElement, rhs: SessionReplayInvisibleElement) -> Bool { + // If two elements are indeed InvisibleElement they're InvisibleElement.constant + return true + } +} + +extension SRWireframe { + public func getAsTextWireframe() -> SRTextWireframe? { + if case .textWireframe(let value) = self { + return value + } + return nil + } +} + +private class MockRCTBridge: RCTBridge { + /// We need to override this function that would otherwise try to setup + /// a real bridge and fail as we don't have a bundled JS. + override func setUp() { + // do nothing + } +} From 775503b56483f7ff2cdee12fad0be17aa4147538 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:26:17 +0100 Subject: [PATCH 06/11] Fix imports from SR --- .../ios/Sources/RCTTextViewRecorder.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 474cf217d..3f9f04b50 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -42,9 +42,9 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { public func semantics( of view: UIView, - with attributes: DatadogSessionReplay.SessionReplayViewAttributes, - in context: DatadogSessionReplay.SessionReplayViewTreeRecordingContext - ) -> DatadogSessionReplay.SessionReplayNodeSemantics? { + with attributes: SessionReplayViewAttributes, + in context: SessionReplayViewTreeRecordingContext + ) -> SessionReplayNodeSemantics? { guard let textView = view as? RCTTextView else { return nil } From 9563bb585794f5541a2388757f27e89ade277f9d Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:26:44 +0100 Subject: [PATCH 07/11] Fix subtree strategy for RCTTextView recorder --- .../ios/Sources/RCTTextViewRecorder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index 3f9f04b50..ed8a0819f 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -73,7 +73,7 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { contentRect: shadow.contentFrame ) let node = SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder) - return SessionReplaySpecificElement(subtreeStrategy: .record, nodes: [node]) + return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [node]) } return SessionReplayInvisibleElement.constant } From aa4bded5aca6c1f8d95201faebf97e48cbb20517 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:27:01 +0100 Subject: [PATCH 08/11] Fix SR tests compilation --- .../DatadogSDKReactNativeSessionReplay.podspec | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 7312d109e..7354caed7 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -22,6 +22,7 @@ Pod::Spec.new do |s| s.dependency 'DatadogSessionReplay', '~> 2.4.0' s.test_spec 'Tests' do |test_spec| + test_spec.dependency "React-RCTText" test_spec.source_files = 'ios/Tests/*.swift' end From d1d33ef3ed0112c26390ef9226e6d4a0204ffd69 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:30:33 +0100 Subject: [PATCH 09/11] Add podfile for SR tests --- .../ios/{PodfileForTests => PodfileForSRTests} | 15 +++++++++++++++ 1 file changed, 15 insertions(+) rename example/ios/{PodfileForTests => PodfileForSRTests} (74%) diff --git a/example/ios/PodfileForTests b/example/ios/PodfileForSRTests similarity index 74% rename from example/ios/PodfileForTests rename to example/ios/PodfileForSRTests index eaa582754..64a08d0c0 100644 --- a/example/ios/PodfileForTests +++ b/example/ios/PodfileForSRTests @@ -21,8 +21,23 @@ if linkage != nil use_frameworks! :linkage => linkage.to_sym end +use_modular_headers! + target 'ddSdkReactnativeExample' do + native_ios_sdk_path = ENV['DD_NATIVE_IOS_SDK_PATH'] + if (native_ios_sdk_path) then + pod 'DatadogCore', :path => "#{native_ios_sdk_path}/DatadogCore.podspec" + pod 'DatadogLogs', :path => "#{native_ios_sdk_path}/DatadogLogs.podspec" + pod 'DatadogTrace', :path => "#{native_ios_sdk_path}/DatadogTrace.podspec" + pod 'DatadogInternal', :path => "#{native_ios_sdk_path}/DatadogInternal.podspec" + pod 'DatadogRUM', :path => "#{native_ios_sdk_path}/DatadogRUM.podspec" + pod 'DatadogCrashReporting', :path => "#{native_ios_sdk_path}/DatadogCrashReporting.podspec" + pod 'DatadogWebViewTracking', :path => "#{native_ios_sdk_path}/DatadogWebViewTracking.podspec" + pod 'DatadogSessionReplay', :path => "#{native_ios_sdk_path}/DatadogSessionReplay.podspec" + end + pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests'] + pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests'] config = use_native_modules! From 6ba4bc3a3fed9f8d2deb8b1117bc341f41007ccb Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 16 Nov 2023 16:41:08 +0100 Subject: [PATCH 10/11] Update iOS SDK version --- example-new-architecture/ios/Podfile.lock | 54 ++++++------- example/ios/Podfile.lock | 79 ++++++++++--------- packages/core/DatadogSDKReactNative.podspec | 12 +-- ...DatadogSDKReactNativeSessionReplay.podspec | 2 +- 4 files changed, 74 insertions(+), 73 deletions(-) diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock index 692567406..19bb79785 100644 --- a/example-new-architecture/ios/Podfile.lock +++ b/example-new-architecture/ios/Podfile.lock @@ -1,23 +1,23 @@ PODS: - boost (1.76.0) - CocoaAsyncSocket (7.6.5) - - DatadogCore (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogCrashReporting (2.4.0): - - DatadogInternal (= 2.4.0) + - DatadogCore (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogCrashReporting (2.5.0): + - DatadogInternal (= 2.5.0) - PLCrashReporter (~> 1.11.1) - - DatadogInternal (2.4.0) - - DatadogLogs (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogRUM (2.4.0): - - DatadogInternal (= 2.4.0) + - DatadogInternal (2.5.0) + - DatadogLogs (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogRUM (2.5.0): + - DatadogInternal (= 2.5.0) - DatadogSDKReactNative (2.0.1): - - DatadogCore (~> 2.4.0) - - DatadogCrashReporting (~> 2.4.0) - - DatadogLogs (~> 2.4.0) - - DatadogRUM (~> 2.4.0) - - DatadogTrace (~> 2.4.0) - - DatadogWebViewTracking (~> 2.4.0) + - DatadogCore (~> 2.5.0) + - DatadogCrashReporting (~> 2.5.0) + - DatadogLogs (~> 2.5.0) + - DatadogRUM (~> 2.5.0) + - DatadogTrace (~> 2.5.0) + - DatadogWebViewTracking (~> 2.5.0) - RCT-Folly (= 2021.07.22.00) - RCTRequired - RCTTypeSafety @@ -26,10 +26,10 @@ PODS: - React-RCTFabric - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - DatadogTrace (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogWebViewTracking (2.4.0): - - DatadogInternal (= 2.4.0) + - DatadogTrace (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogWebViewTracking (2.5.0): + - DatadogInternal (= 2.5.0) - DoubleConversion (1.1.6) - FBLazyVector (0.71.10) - FBReactNativeSpec (0.71.10): @@ -959,14 +959,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 57d2868c099736d80fcd648bf211b4431e51a558 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - DatadogCore: 67ce2f8cd2f58cc90bdee5486d847b49e4b31b0b - DatadogCrashReporting: 0d4bd014714946be77a1ed2d34897036892f2b7b - DatadogInternal: 5789bca7a0284b20655ba2a79738ac7d0cd56e70 - DatadogLogs: 3b8c8778c32b780f916c2894b9d2c53bbf590803 - DatadogRUM: 4207d091be536b888719969a7ca078e2c830819a - DatadogSDKReactNative: 9c95bed4eef14236ad675d88d6d3605a41f5d7be - DatadogTrace: ec75b1da1dcf9f5b574481a3773b296e0e1fda38 - DatadogWebViewTracking: e7a5841f001f488fc0240d7cba1a984ab6c86e9a + DatadogCore: a152fbcc24ea1a6b937c9844b1c1d5b86f0a375e + DatadogCrashReporting: 53b458152130de5505901e025e0dd031ce057f31 + DatadogInternal: 96448807156495aa41a9f177b8c849a404618948 + DatadogLogs: 2e67adf2e2cccd84b880b42f52e56cd0b8c7ef82 + DatadogRUM: d807827ad24ae6c738867e853df38c6cb2bb555b + DatadogSDKReactNative: 0b659c7de3043d16465ae3fd6e2d418a5b62261c + DatadogTrace: 703d7572acc1dcda474ab33b7db3d8d67984192c + DatadogWebViewTracking: 87c0c1c9de4da7bb4f9efb87595da6c3d333aba9 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5 FBReactNativeSpec: 33a87f65f1a467d5f63d11d0cc106a10d3b0639d diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b97b71251..30197e7e9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,43 +1,44 @@ PODS: - boost (1.76.0) - - DatadogCore (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogCrashReporting (2.4.0): - - DatadogInternal (= 2.4.0) + - DatadogCore (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogCrashReporting (2.5.0): + - DatadogInternal (= 2.5.0) - PLCrashReporter (~> 1.11.1) - - DatadogInternal (2.4.0) - - DatadogLogs (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogRUM (2.4.0): - - DatadogInternal (= 2.4.0) + - DatadogInternal (2.5.0) + - DatadogLogs (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogRUM (2.5.0): + - DatadogInternal (= 2.5.0) - DatadogSDKReactNative (2.0.1): - - DatadogCore (~> 2.4.0) - - DatadogCrashReporting (~> 2.4.0) - - DatadogLogs (~> 2.4.0) - - DatadogRUM (~> 2.4.0) - - DatadogTrace (~> 2.4.0) - - DatadogWebViewTracking (~> 2.4.0) + - DatadogCore (~> 2.5.0) + - DatadogCrashReporting (~> 2.5.0) + - DatadogLogs (~> 2.5.0) + - DatadogRUM (~> 2.5.0) + - DatadogTrace (~> 2.5.0) + - DatadogWebViewTracking (~> 2.5.0) - React-Core - DatadogSDKReactNative/Tests (2.0.1): - - DatadogCore (~> 2.4.0) - - DatadogCrashReporting (~> 2.4.0) - - DatadogLogs (~> 2.4.0) - - DatadogRUM (~> 2.4.0) - - DatadogTrace (~> 2.4.0) - - DatadogWebViewTracking (~> 2.4.0) + - DatadogCore (~> 2.5.0) + - DatadogCrashReporting (~> 2.5.0) + - DatadogLogs (~> 2.5.0) + - DatadogRUM (~> 2.5.0) + - DatadogTrace (~> 2.5.0) + - DatadogWebViewTracking (~> 2.5.0) - React-Core - DatadogSDKReactNativeSessionReplay (2.0.1): - - DatadogSessionReplay (~> 2.4.0) + - DatadogSessionReplay (~> 2.5.0) - React-Core - DatadogSDKReactNativeSessionReplay/Tests (2.0.1): - - DatadogSessionReplay (~> 2.4.0) + - DatadogSessionReplay (~> 2.5.0) - React-Core - - DatadogSessionReplay (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogTrace (2.4.0): - - DatadogInternal (= 2.4.0) - - DatadogWebViewTracking (2.4.0): - - DatadogInternal (= 2.4.0) + - React-RCTText + - DatadogSessionReplay (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogTrace (2.5.0): + - DatadogInternal (= 2.5.0) + - DatadogWebViewTracking (2.5.0): + - DatadogInternal (= 2.5.0) - DoubleConversion (1.1.6) - FBLazyVector (0.71.10) - FBReactNativeSpec (0.71.10): @@ -569,16 +570,16 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 57d2868c099736d80fcd648bf211b4431e51a558 - DatadogCore: 67ce2f8cd2f58cc90bdee5486d847b49e4b31b0b - DatadogCrashReporting: 0d4bd014714946be77a1ed2d34897036892f2b7b - DatadogInternal: 5789bca7a0284b20655ba2a79738ac7d0cd56e70 - DatadogLogs: 3b8c8778c32b780f916c2894b9d2c53bbf590803 - DatadogRUM: 4207d091be536b888719969a7ca078e2c830819a - DatadogSDKReactNative: b4d2c0926219c7fa0c7fdb3b9faf963b0c37bc4f - DatadogSDKReactNativeSessionReplay: c320633e2dc1f8d6a8656ff60ca9a3d9cf305035 - DatadogSessionReplay: 8d17ac983669b62e3dd9159cf4a04e98fcf9abeb - DatadogTrace: ec75b1da1dcf9f5b574481a3773b296e0e1fda38 - DatadogWebViewTracking: e7a5841f001f488fc0240d7cba1a984ab6c86e9a + DatadogCore: a152fbcc24ea1a6b937c9844b1c1d5b86f0a375e + DatadogCrashReporting: 53b458152130de5505901e025e0dd031ce057f31 + DatadogInternal: 96448807156495aa41a9f177b8c849a404618948 + DatadogLogs: 2e67adf2e2cccd84b880b42f52e56cd0b8c7ef82 + DatadogRUM: d807827ad24ae6c738867e853df38c6cb2bb555b + DatadogSDKReactNative: 3bdadcd1ad69e2fb9fd53b823dddaa8d503edf6f + DatadogSDKReactNativeSessionReplay: 08001fa73bc35a9acb783cd4c4428cc3d22fdeb9 + DatadogSessionReplay: e264895cd8093c22408f518e0c736f6c3ca6882c + DatadogTrace: 703d7572acc1dcda474ab33b7db3d8d67984192c + DatadogWebViewTracking: 87c0c1c9de4da7bb4f9efb87595da6c3d333aba9 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5 FBReactNativeSpec: 90fc1a90b4b7a171e0a7c20ea426c1bf6ce4399c diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec index 840b9946e..c71a9f671 100644 --- a/packages/core/DatadogSDKReactNative.podspec +++ b/packages/core/DatadogSDKReactNative.podspec @@ -19,12 +19,12 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec - s.dependency 'DatadogCore', '~> 2.4.0' - s.dependency 'DatadogLogs', '~> 2.4.0' - s.dependency 'DatadogTrace', '~> 2.4.0' - s.dependency 'DatadogRUM', '~> 2.4.0' - s.dependency 'DatadogCrashReporting', '~> 2.4.0' - s.dependency 'DatadogWebViewTracking', '~> 2.4.0' + s.dependency 'DatadogCore', '~> 2.5.0' + s.dependency 'DatadogLogs', '~> 2.5.0' + s.dependency 'DatadogTrace', '~> 2.5.0' + s.dependency 'DatadogRUM', '~> 2.5.0' + s.dependency 'DatadogCrashReporting', '~> 2.5.0' + s.dependency 'DatadogWebViewTracking', '~> 2.5.0' s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'ios/Tests/*.swift' diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec index 7354caed7..1d2cd1802 100644 --- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec +++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec - s.dependency 'DatadogSessionReplay', '~> 2.4.0' + s.dependency 'DatadogSessionReplay', '~> 2.5.0' s.test_spec 'Tests' do |test_spec| test_spec.dependency "React-RCTText" From 6bcd537133f741aa65b459f38d784f443a88ed64 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 17 Nov 2023 11:54:32 +0100 Subject: [PATCH 11/11] Remove comments in wireframes builder --- .../ios/Sources/RCTTextViewRecorder.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index ed8a0819f..5a6db8466 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -81,19 +81,12 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder { let wireframeID: WireframeID - /// Attributes of the base `UIView`. let attributes: SessionReplayViewAttributes - /// The text. let text: String? - /// The alignment of the text. var textAlignment: NSTextAlignment - /// The color of the text. let textColor: CGColor? - /// The text obfuscator. let textObfuscator: SessionReplayTextObfuscating - /// The font used by the text field. let font: UIFont? - /// The frame of the text content let contentRect: CGRect public var wireframeRect: CGRect {