Skip to content

Commit

Permalink
[SR] Support Jetpack Compose redaction (#3739)
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

* Replace logo with sentry

* formatting

* Faster boundsInWindow for compose

* api dump

* Dont use liveliterals
  • Loading branch information
romtsn authored Oct 9, 2024
1 parent 1607621 commit 6548825
Show file tree
Hide file tree
Showing 20 changed files with 1,006 additions and 132 deletions.
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@

- 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")`
- 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

### Fixes

- Avoid stopping appStartProfiler after application creation ([#3630](https://github.com/getsentry/sentry-java/pull/3630))
- Session Replay: Correctly detect dominant color for `TextView`s with Spans ([#3682](https://github.com/getsentry/sentry-java/pull/3682))
- 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")`
- If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified
- Fix ensure Application Context is used even when SDK is initialized via Activity Context ([#3669](https://github.com/getsentry/sentry-java/pull/3669))
- Fix potential ANRs due to `Calendar.getInstance` usage in Breadcrumbs constructor ([#3736](https://github.com/getsentry/sentry-java/pull/3736))
- Fix potential ANRs due to default integrations ([#3778](https://github.com/getsentry/sentry-java/pull/3778))
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ object Config {
val composeActivity = "androidx.activity:activity-compose:1.4.0"
val composeFoundation = "androidx.compose.foundation:foundation:$composeVersion"
val composeUi = "androidx.compose.ui:ui:$composeVersion"

val composeUiReplay = "androidx.compose.ui:ui:1.5.0" // Note: don't change without testing forwards compatibility
val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:$composeVersion"
val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13"
val composeCoil = "io.coil-kt:coil-compose:2.6.0"

val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.8.2"

Expand Down
54 changes: 51 additions & 3 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ public final class io/sentry/android/replay/BuildConfig {
}

public class io/sentry/android/replay/DefaultReplayBreadcrumbConverter : io/sentry/ReplayBreadcrumbConverter {
public static final field $stable I
public fun <init> ()V
public fun convert (Lio/sentry/Breadcrumb;)Lio/sentry/rrweb/RRWebEvent;
}

public final class io/sentry/android/replay/GeneratedVideo {
public static final field $stable I
public fun <init> (Ljava/io/File;IJ)V
public final fun component1 ()Ljava/io/File;
public final fun component2 ()I
Expand All @@ -26,6 +28,11 @@ public final class io/sentry/android/replay/GeneratedVideo {
public fun toString ()Ljava/lang/String;
}

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 abstract interface class io/sentry/android/replay/Recorder : java/io/Closeable {
public abstract fun pause ()V
public abstract fun resume ()V
Expand All @@ -34,6 +41,7 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos
}

public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
public static final field $stable I
public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion;
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V
Expand All @@ -50,6 +58,7 @@ public final class io/sentry/android/replay/ReplayCache$Companion {
}

public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, java/io/Closeable {
public static final field $stable I
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -78,6 +87,7 @@ public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallb
}

public final class io/sentry/android/replay/ScreenshotRecorderConfig {
public static final field $stable I
public static final field Companion Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion;
public fun <init> (IIFFII)V
public final fun component1 ()I
Expand All @@ -103,6 +113,12 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion {
public final fun from (Landroid/content/Context;Lio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
}

public final class io/sentry/android/replay/SentryReplayModifiers {
public static final field $stable I
public static final field INSTANCE Lio/sentry/android/replay/SentryReplayModifiers;
public final fun getSentryPrivacy ()Landroidx/compose/ui/semantics/SemanticsPropertyKey;
}

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
Expand All @@ -116,12 +132,14 @@ public final class io/sentry/android/replay/ViewExtensionsKt {
}

public final class io/sentry/android/replay/gestures/GestureRecorder : io/sentry/android/replay/OnRootViewsChangedListener {
public static final field $stable I
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/android/replay/gestures/TouchRecorderCallback;)V
public fun onRootViewsChanged (Landroid/view/View;Z)V
public final fun stop ()V
}

public final class io/sentry/android/replay/gestures/ReplayGestureConverter {
public static final field $stable I
public fun <init> (Lio/sentry/transport/ICurrentDateProvider;)V
public final fun convert (Landroid/view/MotionEvent;Lio/sentry/android/replay/ScreenshotRecorderConfig;)Ljava/util/List;
}
Expand All @@ -130,6 +148,19 @@ public abstract interface class io/sentry/android/replay/gestures/TouchRecorderC
public abstract fun onTouchEvent (Landroid/view/MotionEvent;)V
}

public final class io/sentry/android/replay/util/AndroidTextLayout : io/sentry/android/replay/util/TextLayout {
public static final field $stable I
public fun <init> (Landroid/text/Layout;)V
public fun getDominantTextColor ()Ljava/lang/Integer;
public fun getEllipsisCount (I)I
public fun getLineBottom (I)I
public fun getLineCount ()I
public fun getLineStart (I)I
public fun getLineTop (I)I
public fun getLineVisibleEnd (I)I
public fun getPrimaryHorizontal (II)F
}

public class io/sentry/android/replay/util/FixedWindowCallback : android/view/Window$Callback {
public final field delegate Landroid/view/Window$Callback;
public fun <init> (Landroid/view/Window$Callback;)V
Expand Down Expand Up @@ -160,6 +191,17 @@ public class io/sentry/android/replay/util/FixedWindowCallback : android/view/Wi
public fun onWindowStartingActionMode (Landroid/view/ActionMode$Callback;I)Landroid/view/ActionMode;
}

public abstract interface class io/sentry/android/replay/util/TextLayout {
public abstract fun getDominantTextColor ()Ljava/lang/Integer;
public abstract fun getEllipsisCount (I)I
public abstract fun getLineBottom (I)I
public abstract fun getLineCount ()I
public abstract fun getLineStart (I)I
public abstract fun getLineTop (I)I
public abstract fun getLineVisibleEnd (I)I
public abstract fun getPrimaryHorizontal (II)F
}

public abstract interface class io/sentry/android/replay/video/SimpleFrameMuxer {
public abstract fun getVideoTime ()J
public abstract fun isStarted ()Z
Expand All @@ -169,6 +211,7 @@ public abstract interface class io/sentry/android/replay/video/SimpleFrameMuxer
}

public final class io/sentry/android/replay/video/SimpleMp4FrameMuxer : io/sentry/android/replay/video/SimpleFrameMuxer {
public static final field $stable I
public fun <init> (Ljava/lang/String;F)V
public fun getVideoTime ()J
public fun isStarted ()Z
Expand All @@ -178,6 +221,7 @@ public final class io/sentry/android/replay/video/SimpleMp4FrameMuxer : io/sentr
}

public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public static final field $stable I
public static final field Companion Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion;
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand All @@ -195,6 +239,7 @@ public abstract class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public final fun isObscured (Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;)Z
public final fun isVisible ()Z
public final fun setChildren (Ljava/util/List;)V
public final fun setImportantForCaptureToAncestors (Z)V
public final fun setImportantForContentCapture (Z)V
public final fun traverse (Lkotlin/jvm/functions/Function1;)V
}
Expand All @@ -204,20 +249,23 @@ public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Comp
}

public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$GenericViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public static final field $stable I
public fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$ImageViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public static final field $stable I
public fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
public synthetic fun <init> (FFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}

public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$TextViewHierarchyNode : io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
public fun <init> (Landroid/text/Layout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
public synthetic fun <init> (Landroid/text/Layout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public static final field $stable I
public fun <init> (Lio/sentry/android/replay/util/TextLayout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;)V
public synthetic fun <init> (Lio/sentry/android/replay/util/TextLayout;Ljava/lang/Integer;IIFFIIFILio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;ZZZLandroid/graphics/Rect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDominantColor ()Ljava/lang/Integer;
public final fun getLayout ()Landroid/text/Layout;
public final fun getLayout ()Lio/sentry/android/replay/util/TextLayout;
public final fun getPaddingLeft ()I
public final fun getPaddingTop ()I
}
Expand Down
25 changes: 24 additions & 1 deletion sentry-android-replay/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.kotlin.config.KotlinCompilerVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
id("com.android.library")
Expand All @@ -25,9 +26,20 @@ android {
buildConfigField("String", "VERSION_NAME", "\"${project.version}\"")
}

buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = Config.androidComposeCompilerVersion
useLiveLiterals = false
}

buildTypes {
getByName("debug")
getByName("release")
getByName("release") {
consumerProguardFiles("proguard-rules.pro")
}
}

kotlinOptions {
Expand Down Expand Up @@ -65,6 +77,7 @@ kotlin {
dependencies {
api(projects.sentry)

compileOnly(Config.Libs.composeUiReplay)
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))

// tests
Expand All @@ -77,9 +90,19 @@ dependencies {
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.mockitoInline)
testImplementation(Config.TestLibs.awaitility)
testImplementation(Config.Libs.composeActivity)
testImplementation(Config.Libs.composeUi)
testImplementation(Config.Libs.composeCoil)
testImplementation(Config.Libs.composeFoundation)
testImplementation(Config.Libs.composeFoundationLayout)
testImplementation(Config.Libs.composeMaterial)
}

tasks.withType<Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = JavaVersion.VERSION_1_8.toString()
}

tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions.freeCompilerArgs.add("-opt-in=androidx.compose.ui.ExperimentalComposeUiApi")
}
17 changes: 17 additions & 0 deletions sentry-android-replay/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
-keepattributes SourceFile,LineNumberTable

# Rules to detect Images/Icons and redact 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
-dontwarn androidx.compose.ui.graphics.ColorProducer
-dontwarn androidx.compose.foundation.layout.FillElement
-keepnames class androidx.compose.foundation.layout.FillElement
-keepclasseswithmembernames class * {
androidx.compose.ui.graphics.ColorProducer color;
}
# Rules to detect a compose view to parse its hierarchy
-dontwarn androidx.compose.ui.platform.AndroidComposeView
-keepnames class androidx.compose.ui.platform.AndroidComposeView
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.android.replay

import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.semantics
import io.sentry.android.replay.SentryReplayModifiers.SentryPrivacy

public object SentryReplayModifiers {
val SentryPrivacy = SemanticsPropertyKey<String>(
name = "SentryPrivacy",
mergePolicy = { parentValue, _ -> parentValue }
)
}

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

public fun Modifier.sentryReplayIgnore(): Modifier {
return semantics(
properties = {
this[SentryPrivacy] = "ignore"
}
)
}
Loading

0 comments on commit 6548825

Please sign in to comment.