Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# 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 `SentryAndroid.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks.
- The masks will be invalidated at most once per `frameRate` (default 1 fps).

## 8.12.0

### Features
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ public final class io/sentry/android/core/SentryAndroid {
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;)V
public static fun init (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/Sentry$OptionsConfiguration;)V
public static fun init (Landroid/content/Context;Lio/sentry/Sentry$OptionsConfiguration;)V
public static fun replay ()Lio/sentry/IReplayApi;
Comment thread
markushi marked this conversation as resolved.
Outdated
}

public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/SentryDateProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.os.Process;
import android.os.SystemClock;
import io.sentry.ILogger;
import io.sentry.IReplayApi;
import io.sentry.IScopes;
import io.sentry.ISentryLifecycleToken;
import io.sentry.Integration;
Expand Down Expand Up @@ -254,4 +255,9 @@ private static void deduplicateIntegrations(
}
}
}

@NotNull
public static IReplayApi replay() {
return Sentry.getCurrentScopes().getScope().getOptions().getReplayController();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.sentry.DateUtils
import io.sentry.Hint
import io.sentry.ILogger
import io.sentry.ISentryClient
import io.sentry.ReplayController
import io.sentry.Sentry
import io.sentry.Sentry.OptionsConfiguration
import io.sentry.SentryEnvelope
Expand Down Expand Up @@ -540,6 +541,21 @@ class SentryAndroidTest {
assertTrue(optionsRef.eventProcessors.any { it is AnrV2EventProcessor })
}

@Test
fun `replay debug masking is forwarded to replay controller`() {
val replayController = mock<ReplayController>()
fixture.initSut(context = mock<Application>()) { options ->
options.dsn = "https://key@sentry.io/123"
options.setReplayController(replayController)
}

SentryAndroid.replay().enableDebugMaskingOverlay()
verify(replayController).enableDebugMaskingOverlay()

SentryAndroid.replay().disableDebugMaskingOverlay()
verify(replayController).disableDebugMaskingOverlay()
}

private fun prefillScopeCache(options: SentryOptions, cacheDir: String) {
val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() }
val queueFile = QueueFile.Builder(File(scopeDir, BREADCRUMBS_FILENAME)).build()
Expand Down
3 changes: 3 additions & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
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 fun disableDebugMaskingOverlay ()V
public fun enableDebugMaskingOverlay ()V
public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
public final fun getReplayCacheDir ()Ljava/io/File;
public fun getReplayId ()Lio/sentry/protocol/SentryId;
public fun isDebugMaskingOverlayEnabled ()Z
public fun isRecording ()Z
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public class ReplayIntegration(
this.mainLooperHandler = mainLooperHandler ?: MainLooperHandler()
this.gestureRecorderProvider = gestureRecorderProvider
}

private var debugMaskingEnabled: Boolean = false
private lateinit var options: SentryOptions
private var scopes: IScopes? = null
private var recorder: Recorder? = null
Expand Down Expand Up @@ -251,6 +251,16 @@ public class ReplayIntegration(
pauseInternal()
}

override fun enableDebugMaskingOverlay() {
debugMaskingEnabled = true
}

override fun disableDebugMaskingOverlay() {
debugMaskingEnabled = false
}

override fun isDebugMaskingOverlayEnabled(): Boolean = debugMaskingEnabled

private fun pauseInternal() {
lifecycleLock.acquire().use {
if (!isEnabled.get() || !lifecycle.isAllowed(PAUSED)) {
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 (options.replayController.isDebugMaskingOverlayEnabled()) {
debugMasks.addAll(visibleRects)
}
}
return@traverse true
}

if (options.replayController.isDebugMaskingOverlayEnabled()) {
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 (options.replayController.isDebugMaskingOverlayEnabled()) {
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 @@ -867,6 +867,26 @@ class ReplayIntegrationTest {
verify(recorder).resume()
}

@Test
fun `debug masking is disabled by default`() {
val replay = fixture.getSut(
context
)
assertFalse(replay.isDebugMaskingOverlayEnabled)
}

@Test
fun `debug masking can be enabled and disabled`() {
val replay = fixture.getSut(
context
)
replay.enableDebugMaskingOverlay()
assertTrue(replay.isDebugMaskingOverlayEnabled)

replay.disableDebugMaskingOverlay()
assertFalse(replay.isDebugMaskingOverlayEnabled)
}

private fun getSessionCaptureStrategy(options: SentryOptions): SessionCaptureStrategy {
return SessionCaptureStrategy(
options,
Expand Down
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.core.SentryAndroid;
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 -> {
SentryAndroid.replay().enableDebugMaskingOverlay();
});

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
11 changes: 10 additions & 1 deletion sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,11 @@ public abstract interface class io/sentry/IPerformanceSnapshotCollector : io/sen
public abstract fun setup ()V
}

public abstract interface class io/sentry/IReplayApi {
public abstract fun disableDebugMaskingOverlay ()V
public abstract fun enableDebugMaskingOverlay ()V
}

public abstract interface class io/sentry/IScope {
public abstract fun addAttachment (Lio/sentry/Attachment;)V
public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V
Expand Down Expand Up @@ -1567,9 +1572,12 @@ public final class io/sentry/NoOpReplayBreadcrumbConverter : io/sentry/ReplayBre

public final class io/sentry/NoOpReplayController : io/sentry/ReplayController {
public fun captureReplay (Ljava/lang/Boolean;)V
public fun disableDebugMaskingOverlay ()V
public fun enableDebugMaskingOverlay ()V
public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
public static fun getInstance ()Lio/sentry/NoOpReplayController;
public fun getReplayId ()Lio/sentry/protocol/SentryId;
public fun isDebugMaskingOverlayEnabled ()Z
public fun isRecording ()Z
public fun pause ()V
public fun resume ()V
Expand Down Expand Up @@ -2170,10 +2178,11 @@ public abstract interface class io/sentry/ReplayBreadcrumbConverter {
public abstract fun convert (Lio/sentry/Breadcrumb;)Lio/sentry/rrweb/RRWebEvent;
}

public abstract interface class io/sentry/ReplayController {
public abstract interface class io/sentry/ReplayController : io/sentry/IReplayApi {
public abstract fun captureReplay (Ljava/lang/Boolean;)V
public abstract fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
public abstract fun getReplayId ()Lio/sentry/protocol/SentryId;
public abstract fun isDebugMaskingOverlayEnabled ()Z
public abstract fun isRecording ()Z
public abstract fun pause ()V
public abstract fun resume ()V
Expand Down
Loading
Loading