Skip to content

Commit

Permalink
[SR] Change terminology from redact/ignore to mask/unmask (#3741)
Browse files Browse the repository at this point in the history
* WIP

* Compose works

* Custom redaction works for Compose

* Formatting

* Clean up

* Test

* Add tests

* Changelog

* Change terminology from redact/ignore to mask/unmask

* Changelog

* [SR] Mask web and video views (#3775)

* Replace logo with sentry

* Add missing proguard rules

* formatting

* Faster boundsInWindow for compose

* api dump

* Dont use liveliterals

* Remove redundant test

* Increase timeout in failing test
  • Loading branch information
romtsn authored Oct 9, 2024
1 parent 6548825 commit 0ab3bb3
Show file tree
Hide file tree
Showing 22 changed files with 485 additions and 460 deletions.
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@

- Add support for `feedback` envelope header item type ([#3687](https://github.com/getsentry/sentry-java/pull/3687))
- Add breadcrumb.origin field ([#3727](https://github.com/getsentry/sentry-java/pull/3727))
- Session Replay: Add options to selectively redact/ignore views from being captured. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689))
- `android:tag="sentry-redact|sentry-ignore"` in XML or `view.setTag("sentry-redact|sentry-ignore")` in code tags
- if you already have a tag set for a view, you can set a tag by id: `<tag android:id="@id/sentry_privacy" android:value="redact|ignore"/>` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "redact|ignore")` in code
- `view.sentryReplayRedact()` or `view.sentryReplayIgnore()` extension functions
- redact/ignore `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addRedactViewClass()` or `options.experimental.sessionReplay.addIgnoreViewClass()`. Note, that all of the view subclasses/subtypes will be redacted/ignored as well
- For example, (this is already a default behavior) to redact all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addRedactViewClass("android.widget.TextView")`
- Session Replay: Add options to selectively mask/unmask views captured in replay. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689))
- `android:tag="sentry-mask|sentry-unmask"` in XML or `view.setTag("sentry-mask|sentry-unmask")` in code tags
- if you already have a tag set for a view, you can set a tag by id: `<tag android:id="@id/sentry_privacy" android:value="mask|unmask"/>` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "mask|unmask")` in code
- `view.sentryReplayMask()` or `view.sentryReplayUnmask()` extension functions
- mask/unmask `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addMaskViewClass()` or `options.experimental.sessionReplay.addUnmaskViewClass()`. Note, that all of the view subclasses/subtypes will be masked/unmasked as well
- For example, (this is already a default behavior) to mask all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addMaskViewClass("android.widget.TextView")`
- If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified
- Session Replay: Support Jetpack Compose masking ([#3739](https://github.com/getsentry/sentry-java/pull/3739))
- To selectively mask/unmask @Composables, use `Modifier.sentryReplayRedact()` and `Modifier.sentryReplayIgnore()` modifiers
- To selectively mask/unmask @Composables, use `Modifier.sentryReplayMask()` and `Modifier.sentryReplayUnmask()` modifiers
- Session Replay: Mask `WebView`, `VideoView` and `androidx.media3.ui.PlayerView` by default ([#3775](https://github.com/getsentry/sentry-java/pull/3775))

### Fixes

Expand All @@ -29,6 +30,7 @@

- `options.experimental.sessionReplay.errorSampleRate` was renamed to `options.experimental.sessionReplay.onErrorSampleRate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
- Manifest option `io.sentry.session-replay.error-sample-rate` was renamed to `io.sentry.session-replay.on-error-sample-rate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
- Change `redactAllText` and `redactAllImages` to `maskAllText` and `maskAllImages` ([#3741](https://github.com/getsentry/sentry-java/pull/3741))

## 7.14.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ final class ManifestMetadataReader {

static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.on-error-sample-rate";

static final String REPLAYS_REDACT_ALL_TEXT = "io.sentry.session-replay.redact-all-text";
static final String REPLAYS_MASK_ALL_TEXT = "io.sentry.session-replay.mask-all-text";

static final String REPLAYS_REDACT_ALL_IMAGES = "io.sentry.session-replay.redact-all-images";
static final String REPLAYS_MASK_ALL_IMAGES = "io.sentry.session-replay.mask-all-images";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}
Expand Down Expand Up @@ -409,12 +409,12 @@ static void applyMetadata(
options
.getExperimental()
.getSessionReplay()
.setRedactAllText(readBool(metadata, logger, REPLAYS_REDACT_ALL_TEXT, true));
.setMaskAllText(readBool(metadata, logger, REPLAYS_MASK_ALL_TEXT, true));

options
.getExperimental()
.getSessionReplay()
.setRedactAllImages(readBool(metadata, logger, REPLAYS_REDACT_ALL_IMAGES, true));
.setMaskAllImages(readBool(metadata, logger, REPLAYS_MASK_ALL_IMAGES, true));
}

options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1465,29 +1465,29 @@ class ManifestMetadataReaderTest {
}

@Test
fun `applyMetadata reads session replay redact flags to options`() {
fun `applyMetadata reads session replay mask flags to options`() {
// Arrange
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_REDACT_ALL_TEXT to false, ManifestMetadataReader.REPLAYS_REDACT_ALL_IMAGES to false)
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_MASK_ALL_TEXT to false, ManifestMetadataReader.REPLAYS_MASK_ALL_IMAGES to false)
val context = fixture.getContext(metaData = bundle)

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.ignoreViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.unmaskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}

@Test
fun `applyMetadata reads session replay redact flags to options and keeps default if not found`() {
fun `applyMetadata reads session replay mask flags to options and keeps default if not found`() {
// Arrange
val context = fixture.getContext()

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.redactViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.IMAGE_VIEW_CLASS_NAME))
assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ class SentryAndroidTest {
.untilTrue(asserted)

// assert that persisted values have changed
options.executorService.close(5000L) // finalizes all enqueued persisting tasks
options.executorService.close(10000L) // finalizes all enqueued persisting tasks
assertEquals(
"TestActivity",
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java)
Expand Down
18 changes: 9 additions & 9 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public final class io/sentry/android/replay/GeneratedVideo {
}

public final class io/sentry/android/replay/ModifierExtensionsKt {
public static final fun sentryReplayIgnore (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
public static final fun sentryReplayRedact (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
public static final fun sentryReplayMask (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
public static final fun sentryReplayUnmask (Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
}

public abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable {
Expand Down Expand Up @@ -120,15 +120,15 @@ public final class io/sentry/android/replay/SentryReplayModifiers {
}

public final class io/sentry/android/replay/SessionReplayOptionsKt {
public static final fun getRedactAllImages (Lio/sentry/SentryReplayOptions;)Z
public static final fun getRedactAllText (Lio/sentry/SentryReplayOptions;)Z
public static final fun setRedactAllImages (Lio/sentry/SentryReplayOptions;Z)V
public static final fun setRedactAllText (Lio/sentry/SentryReplayOptions;Z)V
public static final fun getMaskAllImages (Lio/sentry/SentryReplayOptions;)Z
public static final fun getMaskAllText (Lio/sentry/SentryReplayOptions;)Z
public static final fun setMaskAllImages (Lio/sentry/SentryReplayOptions;Z)V
public static final fun setMaskAllText (Lio/sentry/SentryReplayOptions;Z)V
}

public final class io/sentry/android/replay/ViewExtensionsKt {
public static final fun sentryReplayIgnore (Landroid/view/View;)V
public static final fun sentryReplayRedact (Landroid/view/View;)V
public static final fun sentryReplayMask (Landroid/view/View;)V
public static final fun sentryReplayUnmask (Landroid/view/View;)V
}

public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener {
Expand Down Expand Up @@ -230,7 +230,7 @@ public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public final fun getElevation ()F
public final fun getHeight ()I
public final fun getParent ()Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
public final fun getShouldRedact ()Z
public final fun getShouldMask ()Z
public final fun getVisibleRect ()Landroid/graphics/Rect;
public final fun getWidth ()I
public final fun getX ()F
Expand Down
12 changes: 10 additions & 2 deletions sentry-android-replay/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable

# Rules to detect Images/Icons and redact them
# Rules to detect Images/Icons and mask them
-dontwarn androidx.compose.ui.graphics.painter.Painter
-keepnames class * extends androidx.compose.ui.graphics.painter.Painter
-keepclasseswithmembernames class * {
androidx.compose.ui.graphics.painter.Painter painter;
}
# Rules to detect Text colors and if they have Modifier.fillMaxWidth to later redact them
# Rules to detect Text colors and if they have Modifier.fillMaxWidth to later mask them
-dontwarn androidx.compose.ui.graphics.ColorProducer
-dontwarn androidx.compose.foundation.layout.FillElement
-keepnames class androidx.compose.foundation.layout.FillElement
Expand All @@ -18,3 +18,11 @@
# Rules to detect a compose view to parse its hierarchy
-dontwarn androidx.compose.ui.platform.AndroidComposeView
-keepnames class androidx.compose.ui.platform.AndroidComposeView
# Rules to detect a media player view to later mask it
-dontwarn androidx.media3.ui.PlayerView
-keepnames class androidx.media3.ui.PlayerView
# Rules to detect a ExoPlayer view to later mask it
-dontwarn com.google.android.exoplayer2.ui.PlayerView
-keepnames class com.google.android.exoplayer2.ui.PlayerView
-dontwarn com.google.android.exoplayer2.ui.StyledPlayerView
-keepnames class com.google.android.exoplayer2.ui.StyledPlayerView
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ public object SentryReplayModifiers {
)
}

public fun Modifier.sentryReplayRedact(): Modifier {
public fun Modifier.sentryReplayMask(): Modifier {
return semantics(
properties = {
this[SentryPrivacy] = "redact"
this[SentryPrivacy] = "mask"
}
)
}

public fun Modifier.sentryReplayIgnore(): Modifier {
public fun Modifier.sentryReplayUnmask(): Modifier {
return semantics(
properties = {
this[SentryPrivacy] = "ignore"
this[SentryPrivacy] = "unmask"
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ internal class ScreenshotRecorder(
val viewHierarchy = ViewHierarchyNode.fromView(root, null, 0, options)
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.redact") {
recorder.submitSafely(options, "screenshot_recorder.mask") {
val canvas = Canvas(bitmap)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
if (node.shouldRedact && (node.width > 0 && node.height > 0)) {
if (node.shouldMask && (node.width > 0 && node.height > 0)) {
node.visibleRect ?: return@traverse false

// TODO: investigate why it returns true on RN when it shouldn't
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,30 @@ package io.sentry.android.replay

import io.sentry.SentryReplayOptions

// since we don't have getters for redactAllText and redactAllImages, they won't be accessible as
// since we don't have getters for maskAllText and maskAllimages, they won't be accessible as
// properties in Kotlin, therefore we create these extensions where a getter is dummy, but a setter
// delegates to the corresponding method in SentryReplayOptions

/**
* Redact all text content. Draws a rectangle of text bounds with text color on top. By default
* only views extending TextView are redacted.
* Mask all text content. Draws a rectangle of text bounds with text color on top. By default
* only views extending TextView are masked.
*
* <p>Default is enabled.
*/
var SentryReplayOptions.redactAllText: Boolean
var SentryReplayOptions.maskAllText: Boolean
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
get() = error("Getter not supported")
set(value) = setRedactAllText(value)
set(value) = setMaskAllText(value)

/**
* Redact all image content. Draws a rectangle of image bounds with image's dominant color on top.
* Mask all image content. Draws a rectangle of image bounds with image's dominant color on top.
* By default only views extending ImageView with BitmapDrawable or custom Drawable type are
* redacted. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come
* masked. ColorDrawable, InsetDrawable, VectorDrawable are all considered non-PII, as they come
* from the apk.
*
* <p>Default is enabled.
*/
var SentryReplayOptions.redactAllImages: Boolean
var SentryReplayOptions.maskAllImages: Boolean
@Deprecated("Getter is unsupported.", level = DeprecationLevel.ERROR)
get() = error("Getter not supported")
set(value) = setRedactAllImages(value)
set(value) = setMaskAllImages(value)
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package io.sentry.android.replay
import android.view.View

/**
* Marks this view to be redacted in session replay.
* Marks this view to be masked in session replay.
*/
fun View.sentryReplayRedact() {
setTag(R.id.sentry_privacy, "redact")
fun View.sentryReplayMask() {
setTag(R.id.sentry_privacy, "mask")
}

/**
* Marks this view to be ignored from redaction in session.
* Marks this view to be unmasked in session replay.
* All its content will be visible in the replay, use with caution.
*/
fun View.sentryReplayIgnore() {
setTag(R.id.sentry_privacy, "ignore")
fun View.sentryReplayUnmask() {
setTag(R.id.sentry_privacy, "unmask")
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal class ComposeTextLayout(internal val layout: TextLayoutResult, private
// TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime

/**
* This method is necessary to redact images in Compose.
* This method is necessary to mask images in Compose.
*
* We heuristically look up for classes that have a [Painter] modifier, usually they all have a
* `Painter` string in their name, e.g. PainterElement, PainterModifierNodeElement or
Expand Down Expand Up @@ -71,9 +71,9 @@ internal fun LayoutNode.findPainter(): Painter? {
* [androidx.compose.ui.graphics.painter.BrushPainter]
*
* In theory, [androidx.compose.ui.graphics.painter.BitmapPainter] can also come from local assets,
* but it can as well come from a network resource, so we preemptively redact it.
* but it can as well come from a network resource, so we preemptively mask it.
*/
internal fun Painter.isRedactable(): Boolean {
internal fun Painter.isMaskable(): Boolean {
val className = this::class.java.name
return !className.contains("Vector") &&
!className.contains("Color") &&
Expand All @@ -83,11 +83,11 @@ internal fun Painter.isRedactable(): Boolean {
internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean)

/**
* This method is necessary to redact text in Compose.
* This method is necessary to mask text in Compose.
*
* We heuristically look up for classes that have a [Text] modifier, usually they all have a
* `Text` string in their name, e.g. TextStringSimpleElement or TextAnnotatedStringElement. We then
* get the color from the modifier, to be able to redact it with the correct color.
* get the color from the modifier, to be able to mask it with the correct color.
*
* We also look up for classes that have a [Fill] modifier, usually they all have a `Fill` string in
* their name, e.g. FillElement. This is necessary to workaround a Compose bug where single-line
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ internal fun View.isVisibleToUser(): Pair<Boolean, Rect?> {

@SuppressLint("ObsoleteSdkInt")
@TargetApi(21)
internal fun Drawable?.isRedactable(): Boolean {
internal fun Drawable?.isMaskable(): Boolean {
// TODO: maybe find a way how to check if the drawable is coming from the apk or loaded from network
// TODO: otherwise maybe check for the bitmap size and don't redact those that take a lot of height (e.g. a background of a whatsapp chat)
// TODO: otherwise maybe check for the bitmap size and don't mask those that take a lot of height (e.g. a background of a whatsapp chat)
return when (this) {
is InsetDrawable, is ColorDrawable, is VectorDrawable, is GradientDrawable -> false
is BitmapDrawable -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ internal class SimpleVideoEncoder(
)
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.frameRate.toFloat())
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, -1) // use -1 to force always non-key frames, meaning only partial updates to save the video size
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 6) // use 6 to force non-key frames, meaning only partial updates to save the video size. Every 6th second is a key frame, which is useful for buffer mode

format
}
Expand Down
Loading

0 comments on commit 0ab3bb3

Please sign in to comment.