-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Added privacy options: maskViews, maskXMLViewIds, maskImageViews #339
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
fde8f26
Added 2 new privacy options: maskViews, maskXMLViewIds
abelonogov-ld a48a8de
update Readme
abelonogov-ld 3eae847
string variant
abelonogov-ld 7e923ad
address feedback
abelonogov-ld 953257c
address feedback
abelonogov-ld 684a89d
maskImageViews setting
abelonogov-ld a246fa5
Targeted exception
abelonogov-ld ef6b1e1
ignore flaky unit test
abelonogov-ld c57d5c2
support @id/foo case
abelonogov-ld File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
...vability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/MaskViewRef.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.launchdarkly.observability.replay | ||
|
|
||
| import kotlin.reflect.KClass | ||
|
|
||
| sealed interface MaskViewRef { | ||
| val clazz: Class<*> | ||
|
|
||
| data class FromClass( | ||
| override val clazz: Class<*> | ||
| ) : MaskViewRef | ||
|
|
||
| data class FromKClass( | ||
| val kclass: KClass<*> | ||
| ) : MaskViewRef { | ||
| override val clazz: Class<*> = kclass.java | ||
| } | ||
|
|
||
| data class FromName( | ||
| val fullClassName: String | ||
| ) : MaskViewRef { | ||
| override val clazz: Class<*> = | ||
| try { | ||
| Class.forName(fullClassName) | ||
| } catch (e: ClassNotFoundException) { | ||
| throw IllegalArgumentException( | ||
| "PrivacyProfile.maskViews contains an invalid class name: '$fullClassName'. " + | ||
| "Provide a fully-qualified Android View class name (e.g. 'android.widget.TextView').", | ||
| e | ||
| ) | ||
| } | ||
| } | ||
abelonogov-ld marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| fun view(clazz: Class<*>): MaskViewRef = MaskViewRef.FromClass(clazz) | ||
| fun view(kclass: KClass<*>): MaskViewRef = MaskViewRef.FromKClass(kclass) | ||
| fun view(name: String): MaskViewRef = MaskViewRef.FromName(name) | ||
175 changes: 116 additions & 59 deletions
175
...ility-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/PrivacyProfile.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,93 +1,150 @@ | ||
| package com.launchdarkly.observability.replay | ||
|
|
||
| import android.view.View | ||
| import android.widget.ImageView | ||
| import com.launchdarkly.observability.replay.masking.MaskMatcher | ||
| import com.launchdarkly.observability.replay.masking.MaskTarget | ||
|
|
||
| /** | ||
| * [PrivacyProfile] encapsulates options and functionality related to privacy of session | ||
| * replay functionality. | ||
| * [PrivacyProfile] controls what UI elements are masked in session replay. | ||
| * | ||
| * By default, session replay will apply an opaque mask to text inputs, text, and sensitive views. | ||
| * See [sensitiveMatcher] for specific details. | ||
| * Masking is implemented as a list of [MaskMatcher]s that are evaluated against a [MaskTarget]. | ||
| * Targets can represent native Android Views as well as Jetpack Compose semantics nodes. | ||
| * | ||
| * @param maskTextInputs set to false to turn off masking text inputs | ||
| * @param maskText set to false to turn off masking text | ||
| * @param maskSensitive set to false to turn off masking sensitive views | ||
| * @param maskAdditionalMatchers list of additional [com.launchdarkly.observability.replay.masking.MaskMatcher]s that will be masked when they match | ||
| * | ||
| * @param maskTextInputs Set to false to disable masking text input targets. | ||
| * @param maskText Set to false to disable masking text targets. | ||
| * @param maskSensitive Set to false to disable masking "sensitive" targets (password + keyword heuristics). | ||
| * @param maskImageViews Set to true to mask [ImageView] targets by exact class match. | ||
| * @param maskViews Additional Views to mask by exact class match (see [viewsMatcher]). | ||
| * @param maskXMLViewIds Additional Views to mask by resource entry name (see [xmlViewIdsMatcher]). | ||
| * Accepts `"@+id/foo"`, `"@id/foo"`, or `"foo"`. | ||
| * @param maskAdditionalMatchers Additional custom matchers to apply. | ||
| **/ | ||
| data class PrivacyProfile( | ||
| val maskTextInputs: Boolean = true, | ||
| val maskText: Boolean = true, | ||
| val maskSensitive: Boolean = true, | ||
| // only for XML ImageViews | ||
| val maskImageViews: Boolean = false, | ||
| val maskViews: List<MaskViewRef> = emptyList(), | ||
| val maskXMLViewIds: List<String> = emptyList(), | ||
| val maskAdditionalMatchers: List<MaskMatcher> = emptyList(), | ||
| ) { | ||
| private val viewClassSet = buildSet { | ||
| addAll(maskViews.map { it.clazz }) | ||
| if (maskImageViews) add(ImageView::class.java) | ||
| } | ||
|
|
||
| private val maskXMLViewIdSet = maskXMLViewIds.map { | ||
| when { | ||
| it.startsWith("@+id/") -> it.substring(5) | ||
| it.startsWith("@id/") -> it.substring(4) | ||
| else -> it | ||
| } | ||
| }.toSet() | ||
abelonogov-ld marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Converts this [PrivacyProfile] into its equivalent [MaskMatcher] list. | ||
| * | ||
| * Note: matchers are evaluated with `any { ... }`, so ordering only affects performance | ||
| * (earlier matchers can short-circuit later ones). | ||
| */ | ||
| internal fun asMatchersList(): List<MaskMatcher> = buildList { | ||
| // Prefer cheaper checks first; heavier checks should be later. | ||
| if (maskTextInputs) add(textInputMatcher) | ||
| if (maskText) add(textMatcher) | ||
| if (viewClassSet.isNotEmpty()) add(viewsMatcher) | ||
| if (maskXMLViewIdSet.isNotEmpty()) add(xmlViewIdsMatcher) | ||
| if (maskSensitive) add(sensitiveMatcher) | ||
| addAll(maskAdditionalMatchers) | ||
| } | ||
|
|
||
| companion object { | ||
| /** | ||
| * This matcher will match most text inputs, but there may be special cases where it will | ||
| * miss as we can't account for all possible future semantic properties. | ||
| */ | ||
| val textInputMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isTextInput() | ||
| } | ||
| /** | ||
| * Matches targets whose underlying Android View has an exact class match with [maskViews]. | ||
| * | ||
| * Note: this uses `target.view.javaClass` equality; it does not match subclasses. | ||
| */ | ||
| val viewsMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return viewClassSet.contains(target.view.javaClass) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This matcher will match most text, but there may be special cases where it will | ||
| * miss as we can't account for all possible future semantic properties. | ||
| */ | ||
| val textMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isText() | ||
| } | ||
| /** | ||
| * Matches targets whose underlying Android View's resource entry name is included in | ||
| * [maskXMLViewIds]. | ||
| * | ||
| * IDs are compared using `resources.getResourceEntryName(view.id)`, so this only applies to | ||
| * Views with a non-[View.NO_ID] id that resolves to a resource entry. | ||
| */ | ||
| val xmlViewIdsMatcher: MaskMatcher = object : MaskMatcher { | ||
| fun View.idNameOrNull(): String? = | ||
| if (id == View.NO_ID) null | ||
| else runCatching { resources.getResourceEntryName(id) }.getOrNull() | ||
|
|
||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| val id = target.view.idNameOrNull() ?: return false | ||
|
|
||
| return maskXMLViewIdSet.contains(id) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This matcher will match all items having the semantic property [SemanticsProperties.Password] | ||
| * and all text or context descriptions that have substring matches with any of the [sensitiveKeywords] | ||
| */ | ||
| val sensitiveMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isSensitive(sensitiveKeywords) | ||
| } | ||
| /** | ||
| * This matcher will match most text inputs, but there may be special cases where it will | ||
| * miss as we can't account for all possible future semantic properties. | ||
| */ | ||
| val textInputMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isTextInput() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This matcher will match most text, but there may be special cases where it will | ||
| * miss as we can't account for all possible future semantic properties. | ||
| */ | ||
| val textMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isText() | ||
| } | ||
| } | ||
|
|
||
| // this list of sensitive keywords is used to detect sensitive content descriptions | ||
| private val sensitiveKeywords = listOf( | ||
| "sensitive", | ||
| "private", | ||
| "name", | ||
| "email", | ||
| "username", | ||
| "cell", | ||
| "mobile", | ||
| "phone", | ||
| "address", | ||
| "street", | ||
| "dob", | ||
| "birth", | ||
| "password", | ||
| "account", | ||
| "ssn", | ||
| "social", | ||
| "security", | ||
| "credit", | ||
| "debit", | ||
| "card", | ||
| "cvv", | ||
| "mm/yy", | ||
| "pin", | ||
| ) | ||
| /** | ||
| * This matcher will match all items having the semantic property | ||
| * and all text or context descriptions that have substring matches with any of the [sensitiveKeywords] | ||
| */ | ||
| val sensitiveMatcher: MaskMatcher = object : MaskMatcher { | ||
| override fun isMatch(target: MaskTarget): Boolean { | ||
| return target.isSensitive(sensitiveKeywords) | ||
| } | ||
| } | ||
|
|
||
| // this list of sensitive keywords is used to detect sensitive content descriptions | ||
| private val sensitiveKeywords = listOf( | ||
| "sensitive", | ||
| "private", | ||
| "name", | ||
| "email", | ||
| "username", | ||
| "cell", | ||
| "mobile", | ||
| "phone", | ||
| "address", | ||
| "street", | ||
| "dob", | ||
| "birth", | ||
| "password", | ||
| "account", | ||
| "ssn", | ||
| "social", | ||
| "security", | ||
| "credit", | ||
| "debit", | ||
| "card", | ||
| "cvv", | ||
| "mm/yy", | ||
| "pin", | ||
| ) | ||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.