Skip to content

Commit 7d029bd

Browse files
feat: maskWebView option and accesibilityIdentifier support (#44)
- maskWebView option - accesibilityIdentifier support in masking - samples of WKWebView and Storyboards <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds `maskWebViews` privacy option and accessibility identifier-based masking; updates masking logic; and introduces WebView/Storyboard samples with docs updates. > > - **Session Replay SDK**: > - **Privacy options**: Add `privacy.maskWebViews` (default `false`) to `SessionReplayOptions.PrivacyOptions`. > - **Masking logic (`MaskCollector`)**: > - Import WebKit and mask `WKWebView`/`UIWebView` when `maskWebViews` is enabled. > - Honor explicit unmasking and `ignoreAccessibilityIdentifiers`; mask when `maskAccessibilityIdentifiers` matches. > - Adjust text input masking to avoid masking `WKContentView`; simplify image masking to always mask when enabled. > - **Docs**: > - Document `maskWebViews` in README privacy options and configuration examples. > - **TestApp**: > - Update config: include `maskWebViews` and extend `maskAccessibilityIdentifiers`. > - UI: add toggles and sheets for `StoryboardRootView` and `WebViewControllertView`; minor button/layout tweaks; add "Simulate System Under Pressure" navigation link. > - Credit card sample: unmask `nameField`; set `brandChip` accessibility identifier. > - Add new files: `StoryboardRootView`, `StoryboardiOS.storyboard`, `StoryboardiOSViewController`, `WebViewController` (+ SwiftUI wrapper). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit aa46372. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4f9ccf0 commit 7d029bd

File tree

10 files changed

+197
-41
lines changed

10 files changed

+197
-41
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ let config = { () -> LDConfig in
114114
isEnabled: true,
115115
privacy: .init(
116116
maskTextInputs: true,
117+
maskWebViews: false,
117118
maskImages: false,
118119
maskAccessibilityIdentifiers: ["email-field", "password-field"]
119120
)
@@ -159,6 +160,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
159160
Configure privacy settings to control what data is captured:
160161

161162
- **maskTextInputs**: Mask all text input fields (default: `true`)
163+
- **maskWebViews**: Mask contents of Web Views (default: `false`)
162164
- **maskLabels**: Mask all text labels (default: `false`)
163165
- **maskImages**: Mask all images (default: `false`)
164166
- **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask

Sources/SessionReplay/API/SessionReplayOptions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public struct SessionReplayOptions {
55

66
public struct PrivacyOptions {
77
public var maskTextInputs: Bool
8+
public var maskWebViews: Bool
89
public var maskLabels: Bool
910
public var maskImages: Bool
1011

@@ -17,6 +18,7 @@ public struct SessionReplayOptions {
1718
public var minimumAlpha: CGFloat
1819

1920
public init(maskTextInputs: Bool = true,
21+
maskWebViews: Bool = false,
2022
maskLabels: Bool = false,
2123
maskImages: Bool = false,
2224
maskUIViews: [AnyClass] = [],
@@ -25,6 +27,7 @@ public struct SessionReplayOptions {
2527
ignoreAccessibilityIdentifiers: [String] = [],
2628
minimumAlpha: CGFloat = 0.02) {
2729
self.maskTextInputs = maskTextInputs
30+
self.maskWebViews = maskWebViews
2831
self.maskLabels = maskLabels
2932
self.maskImages = maskImages
3033
self.maskUIViews = maskUIViews

Sources/SessionReplay/ScreenCapture/MaskCollector.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import WebKit
23
import UIKit
34
import SwiftUI
45
import Common
@@ -14,6 +15,7 @@ final class MaskCollector {
1415
struct Settings {
1516
var maskiOS26ViewTypes: Set<String>
1617
var maskTextInputs: Bool
18+
var maskWebViews: Bool
1719
var maskImages: Bool
1820
var minimumAlpha: CGFloat
1921
var maskClasses: Set<ObjectIdentifier>
@@ -23,6 +25,7 @@ final class MaskCollector {
2325
init(privacySettings: PrivacySettings) {
2426
self.maskiOS26ViewTypes = Constants.maskiOS26ViewTypes
2527
self.maskTextInputs = privacySettings.maskTextInputs
28+
self.maskWebViews = privacySettings.maskWebViews
2629
self.maskImages = privacySettings.maskImages
2730
self.minimumAlpha = privacySettings.minimumAlpha
2831
self.maskClasses = privacySettings.buildMaskClasses()
@@ -31,16 +34,42 @@ final class MaskCollector {
3134
}
3235

3336
func shouldMask(_ view: UIView) -> Bool {
34-
if maskiOS26ViewTypes.contains(String(describing: type(of: view))) {
37+
if let shouldUnmask = SessionReplayAssociatedObjects.shouldMaskUIView(view),
38+
!shouldUnmask {
39+
return false
40+
}
41+
42+
if let accessibilityIdentifier = view.accessibilityIdentifier,
43+
ignoreAccessibilityIdentifiers.contains(accessibilityIdentifier) {
44+
return false
45+
}
46+
47+
let viewType = type(of: view)
48+
let stringViewType = String(describing: viewType)
49+
50+
if maskiOS26ViewTypes.contains(stringViewType) {
3551
return true
3652
}
3753

38-
if maskTextInputs, let _ = view as? UITextInput {
39-
return SessionReplayAssociatedObjects.shouldMaskUIView(view) ?? true
54+
if maskWebViews {
55+
if let wkWebView = view as? WKWebView {
56+
return true
57+
}
58+
if let uiWebView = view as? UIWebView {
59+
return true
60+
}
61+
}
62+
63+
if maskTextInputs {
64+
if let textInput = view as? UITextInput {
65+
if stringViewType != "WKContentView" {
66+
return true
67+
}
68+
}
4069
}
4170

4271
if maskImages, let imageView = view as? UIImageView {
43-
return SessionReplayAssociatedObjects.shouldMaskUIView(imageView) ?? true
72+
return true
4473
}
4574

4675
if SessionReplayAssociatedObjects.shouldMaskSwiftUI(view) ?? false {
@@ -51,6 +80,11 @@ final class MaskCollector {
5180
return true
5281
}
5382

83+
if let accessibilityIdentifier = view.accessibilityIdentifier,
84+
maskAccessibilityIdentifiers.contains(accessibilityIdentifier) {
85+
return true
86+
}
87+
5488
return false
5589
}
5690
}

TestApp/Sources/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ let config = { () -> LDConfig in
3636
isEnabled: true,
3737
privacy: .init(
3838
maskTextInputs: true,
39+
maskWebViews: false,
3940
maskImages: false,
40-
maskAccessibilityIdentifiers: ["email-field", "password-field"],
41+
maskAccessibilityIdentifiers: ["email-field", "password-field", "card-brand-chip"],
4142
)
4243
))
4344
]

TestApp/Sources/ContentView.swift

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ struct ContentView: View {
2121
@State private var isMaskingUIKitCreditCardEnabled: Bool = false
2222
@State private var isNumberPadEnabled: Bool = false
2323
@State private var isNotebookEnabled: Bool = false
24+
@State private var isStoryboardEnabled: Bool = false
25+
@State private var isWebviewEnabled: Bool = false
2426

2527
@State private var buttonPressed: Bool = false
2628
@State private var errorPressed: Bool = false
@@ -53,27 +55,35 @@ struct ContentView: View {
5355
#endif
5456

5557
FauxLinkToggleRow(title: "Notebook (SwiftUI)", isOn: $isNotebookEnabled)
58+
FauxLinkToggleRow(title: "Storyboad (UIKit)", isOn: $isStoryboardEnabled)
59+
FauxLinkToggleRow(title: "WebView (WebKit)", isOn: $isWebviewEnabled)
5660

57-
Button {
58-
buttonPressed.toggle()
59-
} label: {
60-
Text("span")
61-
}
62-
.buttonStyle(.borderedProminent)
63-
64-
Button {
65-
logsPressed.toggle()
66-
} label: {
67-
Text("logs")
61+
NavigationLink(destination: SystemUnderPressureView()) {
62+
Text("Simulate System Under Pressure")
6863
}
69-
.buttonStyle(.borderedProminent)
7064

71-
Button {
72-
counterMetricPressed.toggle()
73-
} label: {
74-
Text("metric: counter")
65+
HStack {
66+
Button {
67+
buttonPressed.toggle()
68+
} label: {
69+
Text("span")
70+
}
71+
.buttonStyle(.borderedProminent)
72+
73+
Button {
74+
logsPressed.toggle()
75+
} label: {
76+
Text("logs")
77+
}
78+
.buttonStyle(.borderedProminent)
79+
80+
Button {
81+
counterMetricPressed.toggle()
82+
} label: {
83+
Text("metric: counter")
84+
}
85+
.buttonStyle(.borderedProminent)
7586
}
76-
.buttonStyle(.borderedProminent)
7787

7888
Button {
7989
networkPressed.toggle()
@@ -88,27 +98,27 @@ struct ContentView: View {
8898
}
8999
.buttonStyle(.borderedProminent)
90100
.disabled(networkPressed)
91-
92-
Button {
93-
errorPressed.toggle()
94-
} label: {
95-
Text("error")
101+
102+
HStack {
103+
Button {
104+
errorPressed.toggle()
105+
} label: {
106+
Text("error")
107+
}
108+
.buttonStyle(.borderedProminent)
109+
.tint(.red)
110+
111+
Button {
112+
crashPressed.toggle()
113+
} label: {
114+
Text("Crash")
115+
}
116+
.buttonStyle(.borderedProminent)
117+
.tint(.red)
96118
}
97-
.buttonStyle(.borderedProminent)
98-
.tint(.red)
99119

100-
Button {
101-
crashPressed.toggle()
102-
} label: {
103-
Text("Crash")
104-
}
105-
.buttonStyle(.borderedProminent)
106-
.tint(.red)
107-
NavigationLink(destination: SystemUnderPressureView()) {
108-
Text("Simulate System Under Pressure")
109-
}
110120

111-
121+
112122
}.background(Color.clear)
113123
}
114124
.task(id: errorPressed) {
@@ -180,6 +190,10 @@ struct ContentView: View {
180190
MaskingElementsSimpleUIKitView()
181191
}.sheet(isPresented: $isNumberPadEnabled) {
182192
NumberPadView()
193+
}.sheet(isPresented: $isStoryboardEnabled) {
194+
StoryboardRootView()
195+
}.sheet(isPresented: $isWebviewEnabled) {
196+
WebViewControllertView()
183197
}
184198
}
185199
}

TestApp/Sources/SessionReplay/Masking/CreditCardViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public final class CreditCardViewController: UIViewController {
115115
setupLayout()
116116
updateSaveButton()
117117

118-
//nameField.ldUnmask()
118+
nameField.ldUnmask()
119+
brandChip.accessibilityIdentifier = "card-brand-chip"
119120
}
120121

121122
public override func viewDidAppear(_ animated: Bool) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import SwiftUI
2+
import UIKit
3+
4+
struct StoryboardRootView: UIViewControllerRepresentable {
5+
func makeUIViewController(context: Context) -> UIViewController {
6+
let sb = UIStoryboard(name: "StoryboardiOS", bundle: .main)
7+
// Use the Initial VC, or use instantiateViewController(withIdentifier:) if you set an ID
8+
return sb.instantiateInitialViewController()!
9+
}
10+
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24128" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
3+
<device id="retina6_12" orientation="portrait" appearance="light"/>
4+
<dependencies>
5+
<deployment identifier="iOS"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
7+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
8+
<capability name="System colors in document resources" minToolsVersion="11.0"/>
9+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
10+
</dependencies>
11+
<scenes>
12+
<!--StoryboardiOS-->
13+
<scene sceneID="s0d-6b-0kx">
14+
<objects>
15+
<viewController title="StoryboardiOS" id="Y6W-OH-hqX" customClass="StoryboardiOSViewController" customModule="TestApp" sceneMemberID="viewController">
16+
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
17+
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
18+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
19+
<subviews>
20+
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="tM9-Bg-Ve2">
21+
<rect key="frame" x="148" y="118" width="97" height="34"/>
22+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
23+
<fontDescription key="fontDescription" type="system" pointSize="14"/>
24+
<textInputTraits key="textInputTraits"/>
25+
</textField>
26+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h3i-fX-rrY">
27+
<rect key="frame" x="78" y="125" width="45" height="21"/>
28+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
29+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
30+
<nil key="textColor"/>
31+
<nil key="highlightedColor"/>
32+
</label>
33+
</subviews>
34+
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
35+
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
36+
</view>
37+
</viewController>
38+
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
39+
</objects>
40+
<point key="canvasLocation" x="139" y="131"/>
41+
</scene>
42+
</scenes>
43+
<resources>
44+
<systemColor name="systemBackgroundColor">
45+
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
46+
</systemColor>
47+
</resources>
48+
</document>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import UIKit
2+
3+
class StoryboardiOSViewController: UIViewController {
4+
override func viewDidLoad() {
5+
super.viewDidLoad()
6+
view.backgroundColor = .white
7+
}
8+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import UIKit
2+
import WebKit
3+
import SwiftUI
4+
5+
class WebViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
6+
7+
private var webView: WKWebView!
8+
9+
override func loadView() {
10+
11+
// Configure the web view
12+
let webConfiguration = WKWebViewConfiguration()
13+
webView = WKWebView(frame: .zero, configuration: webConfiguration)
14+
webView.uiDelegate = self
15+
webView.navigationDelegate = self
16+
view = webView
17+
}
18+
19+
override func viewDidLoad() {
20+
super.viewDidLoad()
21+
22+
// 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
23+
let myURL = URL(string:"https://launchdarkly.com/")!
24+
let myRequest = URLRequest(url: myURL)
25+
webView.load(myRequest)
26+
}
27+
}
28+
29+
struct WebViewControllertView: UIViewControllerRepresentable {
30+
func makeUIViewController(context: Context) -> UIViewController {
31+
WebViewController()
32+
}
33+
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
34+
}

0 commit comments

Comments
 (0)