Skip to content

Commit

Permalink
Merge 15e61dd into 1fc9aa2
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Apr 5, 2024
2 parents 1fc9aa2 + 15e61dd commit 891e156
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ final class ManifestMetadataReader {

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

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

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

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -376,23 +380,40 @@ static void applyMetadata(
readBool(
metadata, logger, ENABLE_APP_START_PROFILING, options.isEnableAppStartProfiling()));

if (options.getExperimental().getSessionReplayOptions().getSessionSampleRate() == null) {
if (options.getExperimental().getSessionReplay().getSessionSampleRate() == null) {
final Double sessionSampleRate =
readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE);
if (sessionSampleRate != -1) {
options
.getExperimental()
.getSessionReplayOptions()
.setSessionSampleRate(sessionSampleRate);
options.getExperimental().getSessionReplay().setSessionSampleRate(sessionSampleRate);
}
}

if (options.getExperimental().getSessionReplayOptions().getErrorSampleRate() == null) {
if (options.getExperimental().getSessionReplay().getErrorSampleRate() == null) {
final Double errorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE);
if (errorSampleRate != -1) {
options.getExperimental().getSessionReplayOptions().setErrorSampleRate(errorSampleRate);
options.getExperimental().getSessionReplay().setErrorSampleRate(errorSampleRate);
}
}

options
.getExperimental()
.getSessionReplay()
.setRedactAllText(
readBool(
metadata,
logger,
REPLAYS_REDACT_ALL_TEXT,
options.getExperimental().getSessionReplay().getRedactAllText()));

options
.getExperimental()
.getSessionReplay()
.setRedactAllImages(
readBool(
metadata,
logger,
REPLAYS_REDACT_ALL_IMAGES,
options.getExperimental().getSessionReplay().getRedactAllImages()));
}

options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1383,22 +1383,22 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplayOptions.errorSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.errorSampleRate)
}

@Test
fun `applyMetadata does not override replays errorSampleRate from options`() {
// Arrange
val expectedSampleRate = 0.99f
fixture.options.experimental.sessionReplayOptions.errorSampleRate = expectedSampleRate.toDouble()
fixture.options.experimental.sessionReplay.errorSampleRate = expectedSampleRate.toDouble()
val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f)
val context = fixture.getContext(metaData = bundle)

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

// Assert
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplayOptions.errorSampleRate)
assertEquals(expectedSampleRate.toDouble(), fixture.options.experimental.sessionReplay.errorSampleRate)
}

@Test
Expand All @@ -1410,6 +1410,33 @@ class ManifestMetadataReaderTest {
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertNull(fixture.options.experimental.sessionReplayOptions.errorSampleRate)
assertNull(fixture.options.experimental.sessionReplay.errorSampleRate)
}

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

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

// Assert
assertFalse(fixture.options.experimental.sessionReplay.redactAllImages)
assertFalse(fixture.options.experimental.sessionReplay.redactAllText)
}

@Test
fun `applyMetadata reads session replay redact 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.redactAllImages)
assertTrue(fixture.options.experimental.sessionReplay.redactAllText)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ class SentryAndroidTest {
options.release = "prod"
options.dsn = "https://[email protected]/123"
options.isEnableAutoSessionTracking = true
options.experimental.sessionReplayOptions.errorSampleRate = 1.0
options.experimental.sessionReplay.errorSampleRate = 1.0
}

var session: Session? = null
Expand Down
2 changes: 1 addition & 1 deletion sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,6 @@ public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode {
}

public final class io/sentry/android/replay/viewhierarchy/ViewHierarchyNode$Companion {
public final fun fromView (Landroid/view/View;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
public final fun fromView (Landroid/view/View;Lio/sentry/SentryOptions;)Lio/sentry/android/replay/viewhierarchy/ViewHierarchyNode;
}

Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class ReplayIntegration(
private val recorderConfig by lazy(NONE) {
ScreenshotRecorderConfig.from(
context,
options.experimental.sessionReplayOptions
options.experimental.sessionReplay
)
}

Expand All @@ -89,16 +89,16 @@ class ReplayIntegration(
return
}

if (!options.experimental.sessionReplayOptions.isSessionReplayEnabled &&
!options.experimental.sessionReplayOptions.isSessionReplayForErrorsEnabled
if (!options.experimental.sessionReplay.isSessionReplayEnabled &&
!options.experimental.sessionReplay.isSessionReplayForErrorsEnabled
) {
options.logger.log(INFO, "Session replay is disabled, no sample rate specified")
return
}

isFullSession.set(sample(options.experimental.sessionReplayOptions.sessionSampleRate))
isFullSession.set(sample(options.experimental.sessionReplay.sessionSampleRate))
if (!isFullSession.get() &&
!options.experimental.sessionReplayOptions.isSessionReplayForErrorsEnabled
!options.experimental.sessionReplay.isSessionReplayForErrorsEnabled
) {
options.logger.log(INFO, "Session replay is disabled, full session was not sampled and errorSampleRate is not specified")
return
Expand Down Expand Up @@ -182,12 +182,12 @@ class ReplayIntegration(
return
}

if (!sample(options.experimental.sessionReplayOptions.errorSampleRate)) {
if (!sample(options.experimental.sessionReplay.errorSampleRate)) {
options.logger.log(INFO, "Replay wasn't sampled by errorSampleRate, not capturing for event %s", event.eventId)
return
}

val errorReplayDuration = options.experimental.sessionReplayOptions.errorReplayDuration
val errorReplayDuration = options.experimental.sessionReplay.errorReplayDuration
val now = dateProvider.currentTimeMillis
val currentSegmentTimestamp = if (cache?.frames?.isNotEmpty() == true) {
// in buffer mode we have to set the timestamp of the first frame as the actual start
Expand Down Expand Up @@ -277,15 +277,15 @@ class ReplayIntegration(

val now = dateProvider.currentTimeMillis
if (isFullSession.get() &&
(now - segmentTimestamp.get().time >= options.experimental.sessionReplayOptions.sessionSegmentDuration)
(now - segmentTimestamp.get().time >= options.experimental.sessionReplay.sessionSegmentDuration)
) {
val currentSegmentTimestamp = segmentTimestamp.get()
val segmentId = currentSegment.get()
val replayId = currentReplayId.get()

val videoDuration =
createAndCaptureSegment(
options.experimental.sessionReplayOptions.sessionSegmentDuration,
options.experimental.sessionReplay.sessionSegmentDuration,
currentSegmentTimestamp,
replayId,
segmentId
Expand All @@ -296,12 +296,12 @@ class ReplayIntegration(
segmentTimestamp.set(DateUtils.getDateTime(currentSegmentTimestamp.time + videoDuration))
}
} else if (isFullSession.get() &&
(now - replayStartTimestamp.get() >= options.experimental.sessionReplayOptions.sessionDuration)
(now - replayStartTimestamp.get() >= options.experimental.sessionReplay.sessionDuration)
) {
stop()
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
} else if (!isFullSession.get()) {
cache?.rotate(now - options.experimental.sessionReplayOptions.errorReplayDuration)
cache?.rotate(now - options.experimental.sessionReplay.errorReplayDuration)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ internal class ScreenshotRecorder(
return
}

val rootNode = ViewHierarchyNode.fromView(root)
val rootNode = ViewHierarchyNode.fromView(root, options)
root.traverse(rootNode)
pendingViewHierarchy.set(rootNode)

Expand Down Expand Up @@ -227,7 +227,7 @@ internal class ScreenshotRecorder(
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child != null) {
val childNode = ViewHierarchyNode.fromView(child)
val childNode = ViewHierarchyNode.fromView(child, options)
childNodes.add(childNode)
child.traverse(childNode)
}
Expand Down Expand Up @@ -260,7 +260,7 @@ public data class ScreenshotRecorderConfig(

fun from(
context: Context,
sentryReplayOptions: SentryReplayOptions
sessionReplay: SentryReplayOptions
): ScreenshotRecorderConfig {
// PixelCopy takes screenshots including system bars, so we have to get the real size here
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
Expand All @@ -287,8 +287,8 @@ public data class ScreenshotRecorderConfig(
recordingHeight = height,
scaleFactorX = width.toFloat() / screenBounds.width(),
scaleFactorY = height.toFloat() / screenBounds.height(),
frameRate = sentryReplayOptions.frameRate,
bitRate = sentryReplayOptions.bitRate
frameRate = sessionReplay.frameRate,
bitRate = sessionReplay.bitRate
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.ImageView
import android.widget.TextView
import io.sentry.SentryOptions

// TODO: merge with ViewHierarchyNode from sentry-core maybe?
@TargetApi(26)
Expand Down Expand Up @@ -49,14 +50,14 @@ data class ViewHierarchyNode(
// TODO: check if this works on RN
private fun Int.toOpaque() = this or 0xFF000000.toInt()

fun fromView(view: View): ViewHierarchyNode {
fun fromView(view: View, options: SentryOptions): ViewHierarchyNode {
// TODO: Extract redacting into its own class/function
// TODO: extract redacting into a separate thread?
var shouldRedact = false
var dominantColor: Int? = null
var rect: Rect? = null
when (view) {
is TextView -> {
when {
view is TextView && options.experimental.sessionReplay.redactAllText -> {
// TODO: API level check
// TODO: perhaps this is heavy, might reconsider
val nodeInfo = if (VERSION.SDK_INT >= VERSION_CODES.R) {
Expand Down Expand Up @@ -101,7 +102,7 @@ data class ViewHierarchyNode(
}
}

is ImageView -> {
view is ImageView && options.experimental.sessionReplay.redactAllImages -> {
shouldRedact = isVisible(view) && (view.drawable?.isRedactable() ?: false)
if (shouldRedact) {
rect = Rect()
Expand Down
8 changes: 6 additions & 2 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ public abstract interface class io/sentry/EventProcessor {

public final class io/sentry/ExperimentalOptions {
public fun <init> ()V
public fun getSessionReplayOptions ()Lio/sentry/SentryReplayOptions;
public fun setSessionReplayOptions (Lio/sentry/SentryReplayOptions;)V
public fun getSessionReplay ()Lio/sentry/SentryReplayOptions;
public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V
}

public final class io/sentry/ExternalOptions {
Expand Down Expand Up @@ -2571,12 +2571,16 @@ public final class io/sentry/SentryReplayOptions {
public fun getErrorReplayDuration ()J
public fun getErrorSampleRate ()Ljava/lang/Double;
public fun getFrameRate ()I
public fun getRedactAllImages ()Z
public fun getRedactAllText ()Z
public fun getSessionDuration ()J
public fun getSessionSampleRate ()Ljava/lang/Double;
public fun getSessionSegmentDuration ()J
public fun isSessionReplayEnabled ()Z
public fun isSessionReplayForErrorsEnabled ()Z
public fun setErrorSampleRate (Ljava/lang/Double;)V
public fun setRedactAllImages (Z)V
public fun setRedactAllText (Z)V
public fun setSessionSampleRate (Ljava/lang/Double;)V
}

Expand Down
10 changes: 5 additions & 5 deletions sentry/src/main/java/io/sentry/ExperimentalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
* <p>Beware that experimental options can change at any time.
*/
public final class ExperimentalOptions {
private @NotNull SentryReplayOptions sessionReplayOptions = new SentryReplayOptions();
private @NotNull SentryReplayOptions sessionReplay = new SentryReplayOptions();

@NotNull
public SentryReplayOptions getSessionReplayOptions() {
return sessionReplayOptions;
public SentryReplayOptions getSessionReplay() {
return sessionReplay;
}

public void setSessionReplayOptions(final @NotNull SentryReplayOptions sessionReplayOptions) {
this.sessionReplayOptions = sessionReplayOptions;
public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOptions) {
this.sessionReplay = sessionReplayOptions;
}
}
34 changes: 34 additions & 0 deletions sentry/src/main/java/io/sentry/SentryReplayOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ public final class SentryReplayOptions {
*/
private @Nullable Double errorSampleRate;

/**
* Redact all text content. Draws a rectangle of text bounds with text color on top. By default
* only views extending TextView are redacted.
*
* <p>Default is enabled.
*/
private boolean redactAllText = true;

/**
* Redact 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
* from the apk.
*
* <p>Default is enabled.
*/
private boolean redactAllImages = true;

/**
* Defines the quality of the session replay. Higher bit rates have better replay quality, but
* also affect the final payload size to transfer, defaults to 20kbps.
Expand Down Expand Up @@ -87,6 +105,22 @@ public void setSessionSampleRate(final @Nullable Double sessionSampleRate) {
this.sessionSampleRate = sessionSampleRate;
}

public boolean getRedactAllText() {
return redactAllText;
}

public void setRedactAllText(final boolean redactAllText) {
this.redactAllText = redactAllText;
}

public boolean getRedactAllImages() {
return redactAllImages;
}

public void setRedactAllImages(final boolean redactAllImages) {
this.redactAllImages = redactAllImages;
}

@ApiStatus.Internal
public int getBitRate() {
return bitRate;
Expand Down

0 comments on commit 891e156

Please sign in to comment.