Skip to content

Commit

Permalink
REPLAY-1610 CR feedback - rearrange how obfuscators are provided thro…
Browse files Browse the repository at this point in the history
…ugh `context`
  • Loading branch information
ncreated committed May 10, 2023
1 parent 3c7e562 commit 667852a
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,3 @@ public struct SessionReplayConfiguration {
self.customUploadURL = customUploadURL
}
}

/// Session Replay content recording policy.
/// It describes the way in which sensitive content (e.g. text or images) should be captured.
public enum SessionReplayPrivacy {
/// Record all content as it is.
/// When using this option: all text, images and other information will be recorded and presented in the player.
case allowAll

/// Mask all content.
/// When using this option: all characters in texts will be replaced with "x", images will be
/// replaced with placeholders and other content will be masked accordingly, so the original
/// information will not be presented in the player.
///
/// This is the default content policy.
case maskAll

/// Mask input elements, but record all other content as it is.
/// When uing this option: all user input and selected values (text fields, switches, pickers, segmented controls etc.) will be masked,
/// but static text (e.g. in labels) will be not.
case maskUserInput
}

internal extension SessionReplayPrivacy {
/// If input elements should be masked in this privacy mode.
var shouldMaskInputElements: Bool {
switch self {
case .maskAll, .maskUserInput: return true
case .allowAll: return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,10 @@ internal protocol TextObfuscating {
func mask(text: String) -> String
}

/// Text obfuscation strategies for different text types.
internal struct TextObfuscation {
/// Text obfuscator that returns original text (no obfuscation).
private let nop = NOPTextObfuscator()
/// Text obfuscator that replaces each character with `"x"` mask.
private let spacePreservingMask = SpacePreservingMaskObfuscator()
/// Text obfuscator that replaces whole text with fixed-length `"***"` mask (three asterics).
private let fixLengthMask = FixLengthMaskObfuscator()

/// Returns "Sensitive Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Sensitive Text" is:
/// - passwords, e-mails and phone numbers marked in a platform-specific way
/// - AND other forms of sensitivity in text available to each platform
func sensitiveTextObfuscator(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating {
return fixLengthMask
}

/// Returns "Input & Option Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Input & Option Text" is:
/// - a text entered by the user with a keyboard or other text-input device
/// - OR a custom (non-generic) value in selection elements
func inputAndOptionTextObfuscator(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating {
switch privacyLevel {
case .allowAll: return nop
case .maskAll: return fixLengthMask
case .maskUserInput: return fixLengthMask
}
}

/// Returns "Static Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Static Text" is a text not directly entered by the user.
func staticTextObfuscator(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating {
switch privacyLevel {
case .allowAll: return nop
case .maskAll: return spacePreservingMask
case .maskUserInput: return nop
}
}

/// Returns "Hint Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Hint Text" is a static text in editable text elements or option selectors, displayed when there isn't any value set.
func hintTextObfuscator(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating {
switch privacyLevel {
case .allowAll: return nop
case .maskAll: return fixLengthMask
case .maskUserInput: return nop
}
}
}

/// Text obfuscator which replaces all readable characters with space-preserving `"x"` characters.
internal struct SpacePreservingMaskObfuscator: TextObfuscating {
/// The character to mask text with.
let maskCharacter: UnicodeScalar = "x"
private static let maskCharacter: UnicodeScalar = "x"

/// Masks given `text` by replacing all not whitespace characters with `"x"`.
/// - Parameter text: the text to be masked
Expand All @@ -85,7 +31,7 @@ internal struct SpacePreservingMaskObfuscator: TextObfuscating {
case " ", "\n", "\r", "\t":
masked.unicodeScalars.append(nextScalar)
default:
masked.unicodeScalars.append(maskCharacter)
masked.unicodeScalars.append(SpacePreservingMaskObfuscator.maskCharacter)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private struct WheelsStyleDatePickerRecorder {
nodeRecorders: [
UIPickerViewRecorder(
textObfuscator: { context in
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.staticTextObfuscator
}
)
]
Expand All @@ -93,7 +93,7 @@ private struct InlineStyleDatePickerRecorder {
self.viewRecorder = UIViewRecorder()
self.labelRecorder = UILabelRecorder(
textObfuscator: { context in
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.staticTextObfuscator
}
)
self.subtreeRecorder = ViewTreeRecorder(
Expand Down Expand Up @@ -136,7 +136,7 @@ private struct CompactStyleDatePickerRecorder {
UIViewRecorder(),
UILabelRecorder(
textObfuscator: { context in
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.staticTextObfuscator
}
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal class UILabelRecorder: NodeRecorder {
init(
builderOverride: @escaping (UILabelWireframesBuilder) -> UILabelWireframesBuilder = { $0 },
textObfuscator: @escaping (ViewTreeRecordingContext) -> TextObfuscating = { context in
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.staticTextObfuscator
}
) {
self.builderOverride = builderOverride
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal struct UIPickerViewRecorder: NodeRecorder {

init(
textObfuscator: @escaping (ViewTreeRecordingContext) -> TextObfuscating = { context in
return context.textObfuscation.inputAndOptionTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.inputAndOptionTextObfuscator
}
) {
self.selectionRecorder = ViewTreeRecorder(nodeRecorders: [UIViewRecorder()])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import UIKit

internal struct UISegmentRecorder: NodeRecorder {
var textObfuscator: (ViewTreeRecordingContext) -> TextObfuscating = { context in
return context.textObfuscation.inputAndOptionTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.inputAndOptionTextObfuscator
}

func semantics(of view: UIView, with attributes: ViewAttributes, in context: ViewTreeRecordingContext) -> NodeSemantics? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ internal struct UITextFieldRecorder: NodeRecorder {

var textObfuscator: (ViewTreeRecordingContext, _ isSensitive: Bool, _ isPlaceholder: Bool) -> TextObfuscating = { context, isSensitive, isPlaceholder in
if isPlaceholder {
return context.textObfuscation.hintTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.hintTextObfuscator
} else if isSensitive {
return context.textObfuscation.sensitiveTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.sensitiveTextObfuscator
} else {
return context.textObfuscation.inputAndOptionTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.inputAndOptionTextObfuscator
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import UIKit
internal struct UITextViewRecorder: NodeRecorder {
var textObfuscator: (ViewTreeRecordingContext, _ isSensitive: Bool, _ isEditable: Bool) -> TextObfuscating = { context, isSensitive, isEditable in
if isSensitive {
return context.textObfuscation.sensitiveTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.sensitiveTextObfuscator
}

if isEditable {
return context.textObfuscation.inputAndOptionTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.inputAndOptionTextObfuscator
} else {
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
return context.recorder.privacy.staticTextObfuscator
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ internal struct ViewTreeRecordingContext {
let ids: NodeIDGenerator
/// Provides base64 image data with a built in caching mechanism.
let imageDataProvider: ImageDataProviding
/// Available text obfuscators to use accordingly to current privacy mode.
let textObfuscation: TextObfuscation
/// Variable view controller related context
var viewControllerContext: ViewControllerContext = .init()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ internal struct ViewTreeSnapshotBuilder {
let idsGenerator: NodeIDGenerator
/// Provides base64 image data with a built in caching mechanism.
let imageDataProvider: ImageDataProviding
/// Text obfuscation strategies for different text types.
let textObfuscation: TextObfuscation

/// Builds the `ViewTreeSnapshot` for given root view.
///
Expand All @@ -32,8 +30,7 @@ internal struct ViewTreeSnapshotBuilder {
recorder: recorderContext,
coordinateSpace: rootView,
ids: idsGenerator,
imageDataProvider: imageDataProvider,
textObfuscation: textObfuscation
imageDataProvider: imageDataProvider
)
let snapshot = ViewTreeSnapshot(
date: recorderContext.date.addingTimeInterval(recorderContext.rumContext.viewServerTimeOffset ?? 0),
Expand All @@ -50,8 +47,7 @@ extension ViewTreeSnapshotBuilder {
self.init(
viewTreeRecorder: ViewTreeRecorder(nodeRecorders: createDefaultNodeRecorders()),
idsGenerator: NodeIDGenerator(),
imageDataProvider: ImageDataProvider(),
textObfuscation: TextObfuscation()
imageDataProvider: ImageDataProvider()
)
}
}
Expand Down
84 changes: 84 additions & 0 deletions DatadogSessionReplay/Sources/SessionReplayPrivacy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.
*/

/// Session Replay content recording policy.
/// It describes the way in which sensitive content (e.g. text or images) should be captured.
public enum SessionReplayPrivacy {
/// Record all content as it is.
/// When using this option: all text, images and other information will be recorded and presented in the player.
case allowAll

/// Mask all content.
/// When using this option: all characters in texts will be replaced with "x", images will be
/// replaced with placeholders and other content will be masked accordingly, so the original
/// information will not be presented in the player.
///
/// This is the default content policy.
case maskAll

/// Mask input elements, but record all other content as it is.
/// When uing this option: all user input and selected values (text fields, switches, pickers, segmented controls etc.) will be masked,
/// but static text (e.g. in labels) will be not.
case maskUserInput
}

/// Text obfuscation strategies for different text types.
internal extension SessionReplayPrivacy {
/// Returns "Sensitive Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Sensitive Text" is:
/// - passwords, e-mails and phone numbers marked in a platform-specific way
/// - AND other forms of sensitivity in text available to each platform
var sensitiveTextObfuscator: TextObfuscating {
return FixLengthMaskObfuscator()
}

/// Returns "Input & Option Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Input & Option Text" is:
/// - a text entered by the user with a keyboard or other text-input device
/// - OR a custom (non-generic) value in selection elements
var inputAndOptionTextObfuscator: TextObfuscating {
switch self {
case .allowAll: return NOPTextObfuscator()
case .maskAll: return FixLengthMaskObfuscator()
case .maskUserInput: return FixLengthMaskObfuscator()
}
}

/// Returns "Static Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Static Text" is a text not directly entered by the user.
var staticTextObfuscator: TextObfuscating {
switch self {
case .allowAll: return NOPTextObfuscator()
case .maskAll: return SpacePreservingMaskObfuscator()
case .maskUserInput: return NOPTextObfuscator()
}
}

/// Returns "Hint Text" obfuscator for given `privacyLevel`.
///
/// In Session Replay, "Hint Text" is a static text in editable text elements or option selectors, displayed when there isn't any value set.
var hintTextObfuscator: TextObfuscating {
switch self {
case .allowAll: return NOPTextObfuscator()
case .maskAll: return FixLengthMaskObfuscator()
case .maskUserInput: return NOPTextObfuscator()
}
}
}

/// Other convenience helpers.
internal extension SessionReplayPrivacy {
/// If input elements should be masked in this privacy mode.
var shouldMaskInputElements: Bool {
switch self {
case .maskAll, .maskUserInput: return true
case .allowAll: return false
}
}
}
9 changes: 3 additions & 6 deletions DatadogSessionReplay/Tests/Mocks/RecorderMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,24 +285,21 @@ extension ViewTreeRecordingContext: AnyMockable, RandomMockable {
recorder: .mockRandom(),
coordinateSpace: UIView.mockRandom(),
ids: NodeIDGenerator(),
imageDataProvider: mockRandomImageDataProvider(),
textObfuscation: TextObfuscation()
imageDataProvider: mockRandomImageDataProvider()
)
}

static func mockWith(
recorder: Recorder.Context = .mockAny(),
coordinateSpace: UICoordinateSpace = UIView.mockAny(),
ids: NodeIDGenerator = NodeIDGenerator(),
imageDataProvider: ImageDataProviding = MockImageDataProvider(),
textObfuscation: TextObfuscation = TextObfuscation()
imageDataProvider: ImageDataProviding = MockImageDataProvider()
) -> ViewTreeRecordingContext {
return .init(
recorder: recorder,
coordinateSpace: coordinateSpace,
ids: ids,
imageDataProvider: imageDataProvider,
textObfuscation: textObfuscation
imageDataProvider: imageDataProvider
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,6 @@ import XCTest
@testable import DatadogSessionReplay
@testable import TestUtilities

class TextObfuscationTests: XCTestCase {
let obfuscation = TextObfuscation()

func testSensitiveTextObfuscation() {
XCTAssertTrue(obfuscation.sensitiveTextObfuscator(for: .mockRandom()) is FixLengthMaskObfuscator)
}

func testInputAndOptionTextObfuscation() {
XCTAssertTrue(obfuscation.inputAndOptionTextObfuscator(for: .allowAll) is NOPTextObfuscator)
XCTAssertTrue(obfuscation.inputAndOptionTextObfuscator(for: .maskAll) is FixLengthMaskObfuscator)
XCTAssertTrue(obfuscation.inputAndOptionTextObfuscator(for: .maskUserInput) is FixLengthMaskObfuscator)
}

func testStaticTextObfuscation() {
XCTAssertTrue(obfuscation.staticTextObfuscator(for: .allowAll) is NOPTextObfuscator)
XCTAssertTrue(obfuscation.staticTextObfuscator(for: .maskAll) is SpacePreservingMaskObfuscator)
XCTAssertTrue(obfuscation.staticTextObfuscator(for: .maskUserInput) is NOPTextObfuscator) }

func testHintTextObfuscation() {
XCTAssertTrue(obfuscation.hintTextObfuscator(for: .allowAll) is NOPTextObfuscator)
XCTAssertTrue(obfuscation.hintTextObfuscator(for: .maskAll) is FixLengthMaskObfuscator)
XCTAssertTrue(obfuscation.hintTextObfuscator(for: .maskUserInput) is NOPTextObfuscator)
}
}

class SpacePreservingMaskObfuscatorTests: XCTestCase {
let obfuscator = SpacePreservingMaskObfuscator()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ class ViewTreeSnapshotBuilderTests: XCTestCase {
let builder = ViewTreeSnapshotBuilder(
viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]),
idsGenerator: NodeIDGenerator(),
imageDataProvider: MockImageDataProvider(),
textObfuscation: TextObfuscation()
imageDataProvider: MockImageDataProvider()
)

// When
Expand All @@ -40,8 +39,7 @@ class ViewTreeSnapshotBuilderTests: XCTestCase {
let builder = ViewTreeSnapshotBuilder(
viewTreeRecorder: ViewTreeRecorder(nodeRecorders: [nodeRecorder]),
idsGenerator: NodeIDGenerator(),
imageDataProvider: MockImageDataProvider(),
textObfuscation: TextObfuscation()
imageDataProvider: MockImageDataProvider()
)

// When
Expand Down
Loading

0 comments on commit 667852a

Please sign in to comment.