Skip to content

[SR] Change terminology from redact/ignore to mask/unmask #3741

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 24 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading