Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Features

- Add debug mode for Session Replay masking ([#4357](https://github.com/getsentry/sentry-java/pull/4357))
Comment thread
markushi marked this conversation as resolved.
Use `ReplayIntegration.enableDebugMasking()` to overlay the screen with the Session Replay masks
Comment thread
markushi marked this conversation as resolved.
Outdated

## 8.12.0

### Features
Expand Down
7 changes: 7 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {

public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
public static final field $stable I
public static final field Companion Lio/sentry/android/replay/ReplayIntegration$Companion;
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/Function1;)V
public synthetic fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun captureReplay (Ljava/lang/Boolean;)V
public fun close ()V
public static final fun enableDebugMasking ()V
public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
public final fun getReplayCacheDir ()Ljava/io/File;
public fun getReplayId ()Lio/sentry/protocol/SentryId;
Expand All @@ -76,6 +78,11 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
public fun stop ()V
}

public final class io/sentry/android/replay/ReplayIntegration$Companion {
public final fun enableDebugMasking ()V
public final fun getDebugMaskingEnabled ()Z
}

public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallback {
public abstract fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
public abstract fun onScreenshotRecorded (Ljava/io/File;J)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ public class ReplayIntegration(
IConnectionStatusObserver,
IRateLimitObserver {

private companion object {
public companion object {
init {
SentryIntegrationPackageStorage.getInstance()
.addPackage("maven:io.sentry:sentry-android-replay", BuildConfig.VERSION_NAME)
}

public var debugMaskingEnabled: Boolean = false
private set

@JvmStatic
public fun enableDebugMasking() {
debugMaskingEnabled = true
}
}

// needed for the Java's call site
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.android.replay

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.graphics.Bitmap
Expand All @@ -21,6 +22,7 @@ import io.sentry.SentryLevel.INFO
import io.sentry.SentryLevel.WARNING
import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.util.DebugOverlayDrawable
import io.sentry.android.replay.util.MainLooperHandler
import io.sentry.android.replay.util.addOnDrawListenerSafe
import io.sentry.android.replay.util.getVisibleRects
Expand All @@ -37,6 +39,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import kotlin.LazyThreadSafetyMode.NONE
import kotlin.math.roundToInt

@SuppressLint("UseKtx")
@TargetApi(26)
internal class ScreenshotRecorder(
val config: ScreenshotRecorderConfig,
Expand Down Expand Up @@ -70,6 +73,8 @@ internal class ScreenshotRecorder(
private val isCapturing = AtomicBoolean(true)
private val lastCaptureSuccessful = AtomicBoolean(false)

private val debugOverlayDrawable = DebugOverlayDrawable()

fun capture() {
if (!isCapturing.get()) {
if (options.sessionReplay.isDebug) {
Expand Down Expand Up @@ -121,6 +126,8 @@ internal class ScreenshotRecorder(
root.traverse(viewHierarchy, options)

recorder.submitSafely(options, "screenshot_recorder.mask") {
val debugMasks = mutableListOf<Rect>()

val canvas = Canvas(screenshot)
canvas.setMatrix(prescaledMatrix)
viewHierarchy.traverse { node ->
Expand Down Expand Up @@ -158,10 +165,22 @@ internal class ScreenshotRecorder(
visibleRects.forEach { rect ->
canvas.drawRoundRect(RectF(rect), 10f, 10f, maskingPaint)
}
if (ReplayIntegration.debugMaskingEnabled) {
debugMasks.addAll(visibleRects)
}
}
return@traverse true
}

if (ReplayIntegration.debugMaskingEnabled) {
mainLooperHandler.post {
if (debugOverlayDrawable.callback == null) {
root.overlay.add(debugOverlayDrawable)
}
debugOverlayDrawable.updateMasks(debugMasks)
root.postInvalidate()
}
}
screenshotRecorderCallback?.onScreenshotRecorded(screenshot)
lastCaptureSuccessful.set(true)
contentChanged.set(false)
Expand Down Expand Up @@ -194,11 +213,15 @@ internal class ScreenshotRecorder(
// next bind the new root
rootView = WeakReference(root)
root.addOnDrawListenerSafe(this)

// invalidate the flag to capture the first frame after new window is attached
contentChanged.set(true)
}

fun unbind(root: View?) {
if (ReplayIntegration.debugMaskingEnabled) {
root?.overlay?.remove(debugOverlayDrawable)
}
root?.removeOnDrawListenerSafe(this)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package io.sentry.android.replay.util

import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable

internal class DebugOverlayDrawable : Drawable() {

private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val padding = 6f
private val tmpRect = Rect()
private var masks: List<Rect> = emptyList()

companion object {
private val maskBackgroundColor = Color.argb(32, 255, 20, 20)
private val maskBorderColor = Color.argb(128, 255, 20, 20)
private const val TEXT_COLOR = Color.BLACK
private const val TEXT_OUTLINE_COLOR = Color.WHITE

private const val STROKE_WIDTH = 6f
private const val TEXT_SIZE = 32f
}

override fun draw(canvas: Canvas) {
paint.textSize = TEXT_SIZE
paint.setColor(Color.BLACK)

paint.strokeWidth = STROKE_WIDTH

for (mask in masks) {
paint.setColor(maskBackgroundColor)
paint.style = Paint.Style.FILL
canvas.drawRect(mask, paint)

paint.setColor(maskBorderColor)
paint.style = Paint.Style.STROKE
canvas.drawRect(mask, paint)

val topLeftLabel = "${mask.left}/${mask.top}"
paint.getTextBounds(topLeftLabel, 0, topLeftLabel.length, tmpRect)
drawTextWithOutline(
canvas,
topLeftLabel,
mask.left.toFloat(),
mask.top.toFloat()
)

val bottomRightLabel = "${mask.right}/${mask.bottom}"
paint.getTextBounds(bottomRightLabel, 0, bottomRightLabel.length, tmpRect)
drawTextWithOutline(
canvas,
bottomRightLabel,
mask.right.toFloat() - tmpRect.width(),
mask.bottom.toFloat() + tmpRect.height()
)
}
}

private fun drawTextWithOutline(
canvas: Canvas,
bottomRightLabel: String,
Comment thread
markushi marked this conversation as resolved.
x: Float,
y: Float
) {
paint.setColor(TEXT_OUTLINE_COLOR)
paint.style = Paint.Style.STROKE
canvas.drawText(
bottomRightLabel,
x,
y,
paint
)

paint.setColor(TEXT_COLOR)
paint.style = Paint.Style.FILL
canvas.drawText(
bottomRightLabel,
x,
y,
paint
)
}

override fun setAlpha(alpha: Int) {
// no-op
}

override fun setColorFilter(colorFilter: ColorFilter?) {
// no-op
}

@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

fun updateMasks(masks: List<Rect>) {
this.masks = masks
invalidateSelf()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.sentry.ISpan;
import io.sentry.MeasurementUnit;
import io.sentry.Sentry;
import io.sentry.android.replay.ReplayIntegration;
import io.sentry.instrumentation.file.SentryFileOutputStream;
import io.sentry.protocol.Feedback;
import io.sentry.protocol.User;
Expand Down Expand Up @@ -273,6 +274,11 @@ public void run() {
CoroutinesUtil.INSTANCE.throwInCoroutine();
});

binding.enableReplayDebugMode.setOnClickListener(
view -> {
ReplayIntegration.enableDebugMasking();
Comment thread
markushi marked this conversation as resolved.
Outdated
});

setContentView(binding.getRoot());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@
android:layout_height="wrap_content"
android:text="@string/throw_in_coroutine"/>

<Button
android:id="@+id/enable_replay_debug_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_replay_debug_mode"/>

</LinearLayout>

</ScrollView>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<string name="open_metrics">Delightful Developer Metrics</string>
<string name="test_timber_integration">Test Timber</string>
<string name="throw_in_coroutine">Throw exception in coroutine</string>
<string name="enable_replay_debug_mode">Enable Replay Debug Mode</string>
<string name="back_main">Back to Main Activity</string>
<string name="tap_me">text</string>
<string name="lipsum">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nibh lorem, venenatis sed nulla vel, venenatis sodales augue. Mauris varius elit eu ligula volutpat, sed tincidunt orci porttitor. Donec et dignissim lacus, sed luctus ipsum. Praesent ornare luctus tortor sit amet ultricies. Cras iaculis et diam et vulputate. Cras ut iaculis mauris, non pellentesque diam. Nunc in laoreet diam, vitae accumsan eros. Morbi non nunc ac eros molestie placerat vitae id dolor. Quisque ornare aliquam ipsum, a dapibus tortor. In eu sodales tellus.
Expand Down
Loading