From 6742cde2e973f7c34557277901cbfdb66215cf77 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Thu, 23 Oct 2025 17:12:01 -0700 Subject: [PATCH 1/4] - maskWebView option - accesibilityIdentifier support in masking - samples of WKWebView and Storyboards --- .../API/SessionReplayOptions.swift | 3 ++ .../ScreenCapture/MaskCollector.swift | 41 +++++++++++++-- TestApp/Sources/AppDelegate.swift | 3 +- TestApp/Sources/ContentView.swift | 50 +++++++++++-------- .../Masking/CreditCardViewController.swift | 3 +- .../Storyboard/StoryboardRootView.swift | 11 ++++ .../Storyboard/StoryboardiOS.storyboard | 48 ++++++++++++++++++ .../StoryboardiOSViewController.swift | 30 +++++++++++ .../Sources/WebViews/WebViewController.swift | 34 +++++++++++++ 9 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 TestApp/Sources/Storyboard/StoryboardRootView.swift create mode 100644 TestApp/Sources/Storyboard/StoryboardiOS.storyboard create mode 100644 TestApp/Sources/Storyboard/StoryboardiOSViewController.swift create mode 100644 TestApp/Sources/WebViews/WebViewController.swift diff --git a/Sources/SessionReplay/API/SessionReplayOptions.swift b/Sources/SessionReplay/API/SessionReplayOptions.swift index a048ab4..3e5a70d 100644 --- a/Sources/SessionReplay/API/SessionReplayOptions.swift +++ b/Sources/SessionReplay/API/SessionReplayOptions.swift @@ -5,6 +5,7 @@ public struct SessionReplayOptions { public struct PrivacyOptions { public var maskTextInputs: Bool + public var maskWebViews: Bool public var maskLabels: Bool public var maskImages: Bool @@ -17,6 +18,7 @@ public struct SessionReplayOptions { public var minimumAlpha: CGFloat public init(maskTextInputs: Bool = true, + maskWebViews: Bool = false, maskLabels: Bool = false, maskImages: Bool = false, maskUIViews: [AnyClass] = [], @@ -25,6 +27,7 @@ public struct SessionReplayOptions { ignoreAccessibilityIdentifiers: [String] = [], minimumAlpha: CGFloat = 0.02) { self.maskTextInputs = maskTextInputs + self.maskWebViews = maskWebViews self.maskLabels = maskLabels self.maskImages = maskImages self.maskUIViews = maskUIViews diff --git a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift index eb1196a..04f5780 100644 --- a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift +++ b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift @@ -1,4 +1,5 @@ import Foundation +import WebKit import UIKit import SwiftUI import Common @@ -14,6 +15,7 @@ final class MaskCollector { struct Settings { var maskiOS26ViewTypes: Set var maskTextInputs: Bool + var maskWebViews: Bool var maskImages: Bool var minimumAlpha: CGFloat var maskClasses: Set @@ -23,6 +25,7 @@ final class MaskCollector { init(privacySettings: PrivacySettings) { self.maskiOS26ViewTypes = Constants.maskiOS26ViewTypes self.maskTextInputs = privacySettings.maskTextInputs + self.maskWebViews = privacySettings.maskWebViews self.maskImages = privacySettings.maskImages self.minimumAlpha = privacySettings.minimumAlpha self.maskClasses = privacySettings.buildMaskClasses() @@ -31,16 +34,41 @@ final class MaskCollector { } func shouldMask(_ view: UIView) -> Bool { - if maskiOS26ViewTypes.contains(String(describing: type(of: view))) { + if let shouldUnmask = SessionReplayAssociatedObjects.shouldMaskUIView(view), shouldUnmask { + return false + } + + if let accessibilityIdentifier = view.accessibilityIdentifier, + ignoreAccessibilityIdentifiers.contains(accessibilityIdentifier) { + return false + } + + let viewType = type(of: view) + let stringViewType = String(describing: viewType) + + if maskiOS26ViewTypes.contains(stringViewType) { return true } - if maskTextInputs, let _ = view as? UITextInput { - return SessionReplayAssociatedObjects.shouldMaskUIView(view) ?? true + if maskWebViews { + if let wkWebView = view as? WKWebView { + return true + } + if let uiWebView = view as? UIWebView { + return true + } + } + + if maskTextInputs { + if let textInput = view as? UITextInput { + if stringViewType != "WKContentView" { + return true + } + } } if maskImages, let imageView = view as? UIImageView { - return SessionReplayAssociatedObjects.shouldMaskUIView(imageView) ?? true + return true } if SessionReplayAssociatedObjects.shouldMaskSwiftUI(view) ?? false { @@ -51,6 +79,11 @@ final class MaskCollector { return true } + if let accessibilityIdentifier = view.accessibilityIdentifier, + maskAccessibilityIdentifiers.contains(accessibilityIdentifier) { + return true + } + return false } } diff --git a/TestApp/Sources/AppDelegate.swift b/TestApp/Sources/AppDelegate.swift index 9bacea4..3a04f51 100644 --- a/TestApp/Sources/AppDelegate.swift +++ b/TestApp/Sources/AppDelegate.swift @@ -36,8 +36,9 @@ let config = { () -> LDConfig in isEnabled: true, privacy: .init( maskTextInputs: true, + maskWebViews: false, maskImages: false, - maskAccessibilityIdentifiers: ["email-field", "password-field"], + maskAccessibilityIdentifiers: ["email-field", "password-field", "card-brand-chip"], ) )) ] diff --git a/TestApp/Sources/ContentView.swift b/TestApp/Sources/ContentView.swift index cae0710..3149d26 100644 --- a/TestApp/Sources/ContentView.swift +++ b/TestApp/Sources/ContentView.swift @@ -21,6 +21,8 @@ struct ContentView: View { @State private var isMaskingUIKitCreditCardEnabled: Bool = false @State private var isNumberPadEnabled: Bool = false @State private var isNotebookEnabled: Bool = false + @State private var isStoryboardEnabled: Bool = false + @State private var isWebviewEnabled: Bool = false @State private var buttonPressed: Bool = false @State private var errorPressed: Bool = false @@ -53,27 +55,31 @@ struct ContentView: View { #endif FauxLinkToggleRow(title: "Notebook (SwiftUI)", isOn: $isNotebookEnabled) - - Button { - buttonPressed.toggle() - } label: { - Text("span") - } - .buttonStyle(.borderedProminent) - - Button { - logsPressed.toggle() - } label: { - Text("logs") - } - .buttonStyle(.borderedProminent) - - Button { - counterMetricPressed.toggle() - } label: { - Text("metric: counter") + FauxLinkToggleRow(title: "Storyboad (UIKit)", isOn: $isStoryboardEnabled) + FauxLinkToggleRow(title: "WebView (WebKit)", isOn: $isWebviewEnabled) + + HStack { + Button { + buttonPressed.toggle() + } label: { + Text("span") + } + .buttonStyle(.borderedProminent) + + Button { + logsPressed.toggle() + } label: { + Text("logs") + } + .buttonStyle(.borderedProminent) + + Button { + counterMetricPressed.toggle() + } label: { + Text("metric: counter") + } + .buttonStyle(.borderedProminent) } - .buttonStyle(.borderedProminent) Button { networkPressed.toggle() @@ -180,6 +186,10 @@ struct ContentView: View { MaskingElementsSimpleUIKitView() }.sheet(isPresented: $isNumberPadEnabled) { NumberPadView() + }.sheet(isPresented: $isStoryboardEnabled) { + StoryboardRootView() + }.sheet(isPresented: $isWebviewEnabled) { + WebViewControllertView() } } } diff --git a/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift b/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift index d532bb9..d92a1ff 100644 --- a/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift +++ b/TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift @@ -115,7 +115,8 @@ public final class CreditCardViewController: UIViewController { setupLayout() updateSaveButton() - //nameField.ldUnmask() + nameField.ldUnmask() + brandChip.accessibilityIdentifier = "card-brand-chip" } public override func viewDidAppear(_ animated: Bool) { diff --git a/TestApp/Sources/Storyboard/StoryboardRootView.swift b/TestApp/Sources/Storyboard/StoryboardRootView.swift new file mode 100644 index 0000000..a944e62 --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardRootView.swift @@ -0,0 +1,11 @@ +import SwiftUI +import UIKit + +struct StoryboardRootView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + let sb = UIStoryboard(name: "StoryboardiOS", bundle: .main) + // Use the Initial VC, or use instantiateViewController(withIdentifier:) if you set an ID + return sb.instantiateInitialViewController()! + } + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} diff --git a/TestApp/Sources/Storyboard/StoryboardiOS.storyboard b/TestApp/Sources/Storyboard/StoryboardiOS.storyboard new file mode 100644 index 0000000..25e7210 --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardiOS.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift new file mode 100644 index 0000000..f29a73b --- /dev/null +++ b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift @@ -0,0 +1,30 @@ +import UIKit + +class StoryboardiOSViewController: UIViewController { + // MARK: - Outlets + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var nameTextField: UITextField! + // @IBOutlet weak var passwordLabel: UILabel! + // @IBOutlet weak var passwordTextField: UITextField! + // @IBOutlet weak var loginButton: UIButton! + + // MARK: - Actions + @IBAction func loginButtonTapped(_ sender: UIButton) { + // Example login validation logic (add your own as needed) + // let name = nameTextField.text ?? "" + // let password = passwordTextField.text ?? "" + // if name.isEmpty || password.isEmpty { + // // Present an alert or show an error + // let alert = UIAlertController(title: "Missing Info", message: "Please enter both name and password.", preferredStyle: .alert) + // alert.addAction(UIAlertAction(title: "OK", style: .default)) + // present(alert, animated: true) + // return + // } + // Perform login logic here + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + } +} diff --git a/TestApp/Sources/WebViews/WebViewController.swift b/TestApp/Sources/WebViews/WebViewController.swift new file mode 100644 index 0000000..96e680b --- /dev/null +++ b/TestApp/Sources/WebViews/WebViewController.swift @@ -0,0 +1,34 @@ +import UIKit +import WebKit +import SwiftUI + +class WebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate { + + private var webView: WKWebView! + + override func loadView() { + + // Configure the web view + let webConfiguration = WKWebViewConfiguration() + webView = WKWebView(frame: .zero, configuration: webConfiguration) + webView.uiDelegate = self + webView.navigationDelegate = self + view = webView + } + + override func viewDidLoad() { + super.viewDidLoad() + + // The notice should automatically get hidden in the web view as consent is passed from the mobile app to the website. However, it might happen that the notice gets displayed for a very short time before being hidden. You can disable the notice in your web view to make sure that it never shows by appending didomiConfig.notice.enable=false to the query string of the URL that you are loading + let myURL = URL(string:"https://launchdarkly.com/")! + let myRequest = URLRequest(url: myURL) + webView.load(myRequest) + } +} + +struct WebViewControllertView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + WebViewController() + } + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} From b95da8c3fc72d8bdbab63599baa40f74e3998cb7 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Thu, 23 Oct 2025 21:52:53 -0700 Subject: [PATCH 2/4] update doc --- README.md | 2 ++ Sources/SessionReplay/ScreenCapture/MaskCollector.swift | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 344f1c9..f90ff03 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ let config = { () -> LDConfig in isEnabled: true, privacy: .init( maskTextInputs: true, + maskWebViews: false, maskImages: false, maskAccessibilityIdentifiers: ["email-field", "password-field"] ) @@ -159,6 +160,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate { Configure privacy settings to control what data is captured: - **maskTextInputs**: Mask all text input fields (default: `true`) +- **maskWebViews**: Mask contents of Web Views (default: `false`) - **maskLabels**: Mask all text labels (default: `false`) - **maskImages**: Mask all images (default: `false`) - **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask diff --git a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift index 04f5780..fc1fdc3 100644 --- a/Sources/SessionReplay/ScreenCapture/MaskCollector.swift +++ b/Sources/SessionReplay/ScreenCapture/MaskCollector.swift @@ -34,7 +34,8 @@ final class MaskCollector { } func shouldMask(_ view: UIView) -> Bool { - if let shouldUnmask = SessionReplayAssociatedObjects.shouldMaskUIView(view), shouldUnmask { + if let shouldUnmask = SessionReplayAssociatedObjects.shouldMaskUIView(view), + !shouldUnmask { return false } From 2cc53ef4c4ffc4e8cf6f96440e826cd4c2a4b550 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 24 Oct 2025 08:14:50 -0700 Subject: [PATCH 3/4] Delete commented code --- TestApp/Sources/ContentView.swift | 42 ++++++++++--------- .../StoryboardiOSViewController.swift | 18 -------- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/TestApp/Sources/ContentView.swift b/TestApp/Sources/ContentView.swift index 3149d26..7d95023 100644 --- a/TestApp/Sources/ContentView.swift +++ b/TestApp/Sources/ContentView.swift @@ -57,7 +57,11 @@ struct ContentView: View { FauxLinkToggleRow(title: "Notebook (SwiftUI)", isOn: $isNotebookEnabled) FauxLinkToggleRow(title: "Storyboad (UIKit)", isOn: $isStoryboardEnabled) FauxLinkToggleRow(title: "WebView (WebKit)", isOn: $isWebviewEnabled) - + + NavigationLink(destination: SystemUnderPressureView()) { + Text("Simulate System Under Pressure") + } + HStack { Button { buttonPressed.toggle() @@ -94,27 +98,27 @@ struct ContentView: View { } .buttonStyle(.borderedProminent) .disabled(networkPressed) - - Button { - errorPressed.toggle() - } label: { - Text("error") + + HStack { + Button { + errorPressed.toggle() + } label: { + Text("error") + } + .buttonStyle(.borderedProminent) + .tint(.red) + + Button { + crashPressed.toggle() + } label: { + Text("Crash") + } + .buttonStyle(.borderedProminent) + .tint(.red) } - .buttonStyle(.borderedProminent) - .tint(.red) - Button { - crashPressed.toggle() - } label: { - Text("Crash") - } - .buttonStyle(.borderedProminent) - .tint(.red) - NavigationLink(destination: SystemUnderPressureView()) { - Text("Simulate System Under Pressure") - } - + }.background(Color.clear) } .task(id: errorPressed) { diff --git a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift index f29a73b..54ba861 100644 --- a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift +++ b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift @@ -4,24 +4,6 @@ class StoryboardiOSViewController: UIViewController { // MARK: - Outlets @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var nameTextField: UITextField! - // @IBOutlet weak var passwordLabel: UILabel! - // @IBOutlet weak var passwordTextField: UITextField! - // @IBOutlet weak var loginButton: UIButton! - - // MARK: - Actions - @IBAction func loginButtonTapped(_ sender: UIButton) { - // Example login validation logic (add your own as needed) - // let name = nameTextField.text ?? "" - // let password = passwordTextField.text ?? "" - // if name.isEmpty || password.isEmpty { - // // Present an alert or show an error - // let alert = UIAlertController(title: "Missing Info", message: "Please enter both name and password.", preferredStyle: .alert) - // alert.addAction(UIAlertAction(title: "OK", style: .default)) - // present(alert, animated: true) - // return - // } - // Perform login logic here - } override func viewDidLoad() { super.viewDidLoad() From aa463725eb4a7c746a0bfe602830d8727653e96f Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Fri, 24 Oct 2025 09:57:04 -0700 Subject: [PATCH 4/4] Another unused fields --- TestApp/Sources/Storyboard/StoryboardiOSViewController.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift index 54ba861..d60dc1b 100644 --- a/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift +++ b/TestApp/Sources/Storyboard/StoryboardiOSViewController.swift @@ -1,10 +1,6 @@ import UIKit class StoryboardiOSViewController: UIViewController { - // MARK: - Outlets - @IBOutlet weak var nameLabel: UILabel! - @IBOutlet weak var nameTextField: UITextField! - override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white