Skip to content

Commit

Permalink
- recording: mask views with contentDescription setting and mask WebV…
Browse files Browse the repository at this point in the history
…iew if any masking is enabled (#149)
  • Loading branch information
marandaneto authored Jul 2, 2024
1 parent 6b5485b commit c6a5fc6
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- recording: mask views with `contentDescription` setting and mask `WebView` if any masking is enabled ([#149](https://github.com/PostHog/posthog-android/pull/149))

## 3.4.2 - 2024-06-28

- chore: create ctor overloads for better Java DX ([#148](https://github.com/PostHog/posthog-android/pull/148))
Expand Down
2 changes: 1 addition & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ val config = PostHogAndroidConfig(apiKey).apply {
}
```

If you don't want to mask everything, you can disable the mask config above and mask specific views using the `ph-no-capture` tag.
If you don't want to mask everything, you can disable the mask config above and mask specific views using the `ph-no-capture` value in the [android:tag](https://developer.android.com/reference/android/view/View#attr_android:tag) or [android:contentDescription](https://developer.android.com/reference/android/view/View#attr_android:contentDescription)..

```xml
<ImageView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class PostHogLifecycleObserverIntegration(
private val lastUpdatedSession = AtomicLong(0L)
private val sessionMaxInterval = (1000 * 60 * 30).toLong() // 30 minutes

companion object {
private companion object {
// in case there are multiple instances or the SDK is closed/setup again
// the value is still cached
@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ internal class PostHogSharedPreferences(
return preferences
}

companion object {
private companion object {
private val SPECIAL_KEYS = listOf(GROUPS)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -460,17 +460,23 @@ public class PostHogReplayIntegration(
return rect
}

private fun View.isTextInputSensitive(): Boolean {
return isNoCapture(config.sessionReplayConfig.maskAllTextInputs)
}

private fun View.isAnyInputSensitive(): Boolean {
return this.isTextInputSensitive() || config.sessionReplayConfig.maskAllImages
}

private fun TextView.shouldMaskTextView(): Boolean {
// inputType is 0-based
return isNoCapture(config.sessionReplayConfig.maskAllTextInputs) || passwordInputTypes.contains(inputType - 1)
return this.isTextInputSensitive() || passwordInputTypes.contains(inputType - 1)
}

private fun findMaskableWidgets(
view: View,
maskableWidgets: MutableList<Rect>,
) {
var parentMasked = false

if (view is TextView) {
val viewText = view.text?.toString()
var maskIt = false
Expand All @@ -488,48 +494,50 @@ public class PostHogReplayIntegration(
if (maskIt) {
val rect = view.globalVisibleRect()
maskableWidgets.add(rect)
parentMasked = true
return
}
}

if (view is Spinner) {
val maskIt = view.shouldMaskSpinner()

if (maskIt) {
if (view.shouldMaskSpinner()) {
val rect = view.globalVisibleRect()
maskableWidgets.add(rect)
parentMasked = true
return
}
}

if (view is ImageView) {
val maskIt = view.shouldMaskImage()
if (view.shouldMaskImage()) {
val rect = view.globalVisibleRect()
maskableWidgets.add(rect)
return
}
}

if (maskIt) {
if (view is WebView) {
if (view.isAnyInputSensitive()) {
val rect = view.globalVisibleRect()
maskableWidgets.add(rect)
parentMasked = true
return
}
}

// if a view parent of any type is tagged as non masking, mask it
if (!parentMasked && view.isNoCapture()) {
if (view.isNoCapture()) {
val rect = view.globalVisibleRect()
maskableWidgets.add(rect)
parentMasked = true
return
}

if (!parentMasked) {
if (view is ViewGroup && view.childCount > 0) {
for (i in 0 until view.childCount) {
val viewChild = view.getChildAt(i) ?: continue

if (!viewChild.isVisible()) {
continue
}
if (view is ViewGroup && view.childCount > 0) {
for (i in 0 until view.childCount) {
val viewChild = view.getChildAt(i) ?: continue

findMaskableWidgets(viewChild, maskableWidgets)
if (!viewChild.isVisible()) {
continue
}

findMaskableWidgets(viewChild, maskableWidgets)
}
}
}
Expand Down Expand Up @@ -609,7 +617,7 @@ public class PostHogReplayIntegration(
}

private fun Spinner.shouldMaskSpinner(): Boolean {
return isNoCapture(config.sessionReplayConfig.maskAllTextInputs)
return this.isTextInputSensitive()
}

private fun View.toWireframe(parentId: Int? = null): RRWireframe? {
Expand Down Expand Up @@ -1117,7 +1125,8 @@ public class PostHogReplayIntegration(
}

private fun View.isNoCapture(maskInput: Boolean = false): Boolean {
return (tag as? String)?.lowercase()?.contains("ph-no-capture") == true || maskInput
return maskInput || (tag as? String)?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true ||
contentDescription?.toString()?.lowercase()?.contains(PH_NO_CAPTURE_LABEL) == true
}

private fun Drawable.copy(): Drawable? {
Expand All @@ -1132,4 +1141,8 @@ public class PostHogReplayIntegration(
private fun isSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}

private companion object {
private const val PH_NO_CAPTURE_LABEL = "ph-no-capture"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal class NextDrawListener(
}
}

companion object {
internal companion object {
// only call if onDecorViewReady
internal fun View.onNextDraw(
mainHandler: MainHandler,
Expand Down

0 comments on commit c6a5fc6

Please sign in to comment.