Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPLAY-1610 Align text and appearance masking rules #1279

Merged
merged 7 commits into from
May 10, 2023

Conversation

ncreated
Copy link
Member

@ncreated ncreated commented May 8, 2023

What and why?

📦 This PR aligns rules of text and appearance masking to what was concluded in Privacy Options in Mobile SR:

Screenshot 2023-05-08 at 11 31 55

How?

For text masking, above configuration is reflected in SessionReplayPrivacy extension:

/// Text masking strategies per type of text.
extension SessionReplayPrivacy {
    var sensitiveTextObfuscator: TextObfuscating
    var inputAndOptionTextObfuscator: TextObfuscating
    var staticTextObfuscator: TextObfuscating
    var hintTextObfuscator: TextObfuscating
}

Each node recorder uses TextObfuscation to obtain strategy respectively to its text type (e.g. label recorder uses staticTextObfuscator(for:)). Same as before, compound recorders can configure child recorders with alternative strategies (e.g. label recorder in picker view recorder uses inputAndOptionTextObfuscator(for:)).

For appearance masking, we had to update the UISliderRecorder (other recorders were fine) so it erases selected value in .maskAll and .maskUserInput modes:

`.allowAll``.maskAll``.maskUserInput`

No changes to touch interaction masking - it's already good.


This PR alters most of the snapshot files, but due to their number only few representative examples:

`.allowAll``.maskAll``.maskUserInput`

Review checklist

  • Feature or bugfix MUST have appropriate tests (unit, integration)
  • Make sure each commit and the PR mention the Issue number or JIRA reference
  • Add CHANGELOG entry for user facing changes

Custom CI job configuration (optional)

  • Run unit tests
  • Run integration tests
  • Run smoke tests

@ncreated ncreated self-assigned this May 8, 2023
@ncreated ncreated force-pushed the ncreated/REPLAY-1610/align-privacy-rules branch from 6b2b1af to f207377 Compare May 8, 2023 08:54
case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
Copy link
Member Author

Choose a reason for hiding this comment

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

💡 Passing context.*.privacy to context.textObfuscation.*() might look odd for a moment, but it will get more sense once per-element masking is available later in fine grained privacy project:

context.textObfuscation.staticTextObfuscator(for: overridenPrivacy ?? context.recorder.privacy)

Copy link
Member

Choose a reason for hiding this comment

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

To me, TextObfuscation as an object is kind of superfluous: it's not mockable and makes the call stack a bit odd as you mentioned. I think it would make more sense as a SessionReplayPrivacy extension:

// TextObfuscator.swift

extension SessionReplayPrivacy {
    /// Returns "Static Text" obfuscator for this privacy level.
    ///
    /// 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()
        }
    }
}

Then this closure would simply read:

(overridenPrivacy ?? context.recorder.privacy).staticTextObfuscator

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think, we don't have a right abstraction here. I kind of liked what Max proposed but with slightly protocol oriented.

protocol TextObfuscator {
    func obfuscate(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating
}
struct sensitiveTextObfuscator: TextObfuscator {
    func obfuscate(for privacyLevel: SessionReplayPrivacy) -> TextObfuscating {
        ...
    }
}

Usage

context.textObfuscator.obfuscate(for: context.recorder.privacy)

Copy link
Member Author

Choose a reason for hiding this comment

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

@maxep there is the performance characteristics we anticipate in SpacePreservingMaskObfuscator that could eventually make this idea slightly less convenient. It works O(n) today, but O(1) is possible with LRU-like caching. It runs on background thread, so it's not that much of a problem, but anticipating apps with long texts (e.g. social media posts), it could make sense to cache it. If adding caching, it the context.textObfuscator would become truer dependency injection mechanism.

However, thinking twice I tend to realise that it might be pre-mature. Adding cache to SPMObfuscator is not impossible with your proposal, so I updated code with what you proposed. I agree that it results with more consistent domain definition 👌.


@ganeshnj if I get the idea right, this won't be correct to our problematic:

context.textObfuscator.obfuscate(for: context.recorder.privacy)

Some cases are more complex as this, e.g. text field masking is currently:

if isPlaceholder {
    return context.recorder.privacy.hintTextObfuscator
} else if isSensitive {
    return context.recorder.privacy.sensitiveTextObfuscator
} else {
    return context.recorder.privacy.inputAndOptionTextObfuscator
}

so it can't use single context.textObfuscator with different implementations. There is also a challenge of elements that need to be masked differently in different subtrees, described in more details in #1237 (2nd point in "How?").

I think we're "good enough" with @maxep 's proposal.

Copy link
Contributor

Choose a reason for hiding this comment

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

for my understanding

are isPlaceholder, isSensitive set before the obfuscator is decided or later?

Copy link
Member Author

Choose a reason for hiding this comment

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

They are set before (we infer both from UITextField instance). Based on that, we decide if the text should be classified as "Hint Text", "Sensitive Text" or "Input & Option Text" according to Privacy Options in Mobile SR. The category of the text and current privacy are then used to decide on which obfuscator implementation will be used: NOP, FixLength or SpacePreserving.

Copy link
Member Author

Choose a reason for hiding this comment

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

💡 Here, I'm enhancing the BasicTexts fixture. Now it shows TextView in editable and non-editable state, as both are masked differently (respectively as "Input & Option Text" and "Static Text"), e.g.:

`.allowAll``.maskUserInput`

@ncreated ncreated changed the title REPLAY-1610 Align text obfuscation and privacy rules REPLAY-1610 Align text obfuscation and appearance masking rules May 8, 2023
@ncreated ncreated changed the title REPLAY-1610 Align text obfuscation and appearance masking rules REPLAY-1610 Align text and appearance masking rules May 8, 2023
@ncreated ncreated marked this pull request as ready for review May 8, 2023 09:52
@ncreated ncreated requested a review from a team as a code owner May 8, 2023 09:52
Copy link
Member

@maciejburda maciejburda left a comment

Choose a reason for hiding this comment

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

LGTM! 🎉

Copy link
Member

@maxep maxep left a comment

Choose a reason for hiding this comment

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

Looks great!

I've left a suggestion regarding TextObfuscation which could be replaced by a SessionReplayPrivacy extension. It would fit better in the recorders design. LMKWYT!

case .maskAll: return context.textObfuscators.spacePreservingMask
case .maskUserInput: return context.textObfuscators.spacePreservingMask
}
return context.textObfuscation.staticTextObfuscator(for: context.recorder.privacy)
Copy link
Member

Choose a reason for hiding this comment

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

To me, TextObfuscation as an object is kind of superfluous: it's not mockable and makes the call stack a bit odd as you mentioned. I think it would make more sense as a SessionReplayPrivacy extension:

// TextObfuscator.swift

extension SessionReplayPrivacy {
    /// Returns "Static Text" obfuscator for this privacy level.
    ///
    /// 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()
        }
    }
}

Then this closure would simply read:

(overridenPrivacy ?? context.recorder.privacy).staticTextObfuscator

WDYT?

Comment on lines 20 to 21
/// Available text obfuscators to use by different recorders in response to current privacy mode.
let textObfuscators: TextObfuscators
/// Text obfuscation strategies for different text types.
let textObfuscation: TextObfuscation
Copy link
Member

Choose a reason for hiding this comment

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

TextObfuscation is not an abstraction so we don't get much value of injecting it, using an SessionReplayPrivacy extension as mentioned would simplify things.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree - although as explained, I was anticipating adding cache later. That way, the VTSBuilder would orchestrate it. Anyway, this was revamped according to your proposal ✅

@ncreated ncreated requested a review from maxep May 10, 2023 10:20
Copy link
Member

@maxep maxep left a comment

Choose a reason for hiding this comment

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

Nice work @ncreated ! and kudos for the great description and comments 🙏

@ncreated ncreated merged commit 91f9146 into develop May 10, 2023
@ncreated ncreated deleted the ncreated/REPLAY-1610/align-privacy-rules branch May 10, 2023 13:15
@ncreated ncreated mentioned this pull request May 17, 2023
6 tasks
@ncreated ncreated mentioned this pull request May 24, 2023
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants