Skip to content
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
2 changes: 1 addition & 1 deletion sentry-android-core/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@

##---------------Begin: proguard configuration for sentry-android-replay ----------
-dontwarn io.sentry.android.replay.ReplayIntegration
-dontwarn io.sentry.android.replay.ReplayIntegrationKt
-dontwarn io.sentry.android.replay.DefaultReplayBreadcrumbConverter
-keepnames class io.sentry.android.replay.ReplayIntegration
##---------------End: proguard configuration for sentry-android-replay ----------
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
import io.sentry.android.replay.DefaultReplayBreadcrumbConverter;
import io.sentry.android.replay.ReplayIntegration;
import io.sentry.android.timber.SentryTimberIntegration;
import io.sentry.cache.PersistingOptionsObserver;
Expand Down Expand Up @@ -308,6 +309,7 @@ static void installDefaultIntegrations(
if (isReplayAvailable) {
final ReplayIntegration replay =
new ReplayIntegration(context, CurrentDateProvider.getInstance());
replay.setBreadcrumbConverter(new DefaultReplayBreadcrumbConverter());
options.addIntegration(replay);
options.setReplayController(replay);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ public final class SentryAndroid {
static final String SENTRY_REPLAY_INTEGRATION_CLASS_NAME =
"io.sentry.android.replay.ReplayIntegration";

private static boolean isReplayAvailable = false;

private static final String TIMBER_CLASS_NAME = "timber.log.Timber";
private static final String FRAGMENT_CLASS_NAME =
"androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks";
Expand Down Expand Up @@ -104,7 +102,7 @@ public static synchronized void init(
final boolean isTimberAvailable =
(isTimberUpstreamAvailable
&& classLoader.isClassAvailable(SENTRY_TIMBER_INTEGRATION_CLASS_NAME, options));
isReplayAvailable =
final boolean isReplayAvailable =
classLoader.isClassAvailable(SENTRY_REPLAY_INTEGRATION_CLASS_NAME, options);

final BuildInfoProvider buildInfoProvider = new BuildInfoProvider(logger);
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 @@ -6,6 +6,11 @@ public final class io/sentry/android/replay/BuildConfig {
public fun <init> ()V
}

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

public final class io/sentry/android/replay/GeneratedVideo {
public fun <init> (Ljava/io/File;IJ)V
public final fun component1 ()Ljava/io/File;
Expand Down Expand Up @@ -42,6 +47,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
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 close ()V
public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter;
public final fun getReplayCacheDir ()Ljava/io/File;
public fun getReplayId ()Lio/sentry/protocol/SentryId;
public fun isRecording ()Z
Expand All @@ -55,6 +61,7 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
public fun resume ()V
public fun sendReplay (Ljava/lang/Boolean;Ljava/lang/String;Lio/sentry/Hint;)V
public fun sendReplayForEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)V
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
public fun start ()V
public fun stop ()V
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package io.sentry.android.replay

import io.sentry.Breadcrumb
import io.sentry.ReplayBreadcrumbConverter
import io.sentry.SentryLevel
import io.sentry.SpanDataConvention
import io.sentry.rrweb.RRWebBreadcrumbEvent
import io.sentry.rrweb.RRWebEvent
import io.sentry.rrweb.RRWebSpanEvent
import kotlin.LazyThreadSafetyMode.NONE

public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
internal companion object {
private val snakecasePattern by lazy(NONE) { "_[a-z]".toRegex() }
private val supportedNetworkData = setOf(
"status_code",
"method",
"response_content_length",
"request_content_length",
"http.response_content_length",
"http.request_content_length"
)
}

override fun convert(breadcrumb: Breadcrumb): RRWebEvent? {
var breadcrumbMessage: String? = null
var breadcrumbCategory: String? = null
var breadcrumbLevel: SentryLevel? = null
val breadcrumbData = mutableMapOf<String, Any?>()
when {
breadcrumb.category == "http" -> {
return if (breadcrumb.isValidForRRWebSpan()) breadcrumb.toRRWebSpanEvent() else null
}

breadcrumb.type == "navigation" &&
breadcrumb.category == "app.lifecycle" -> {
breadcrumbCategory = "app.${breadcrumb.data["state"]}"
}

breadcrumb.type == "navigation" &&
breadcrumb.category == "device.orientation" -> {
breadcrumbCategory = breadcrumb.category!!
val position = breadcrumb.data["position"]
if (position == "landscape" || position == "portrait") {
breadcrumbData["position"] = position
} else {
return null
}
}

breadcrumb.type == "navigation" -> {
breadcrumbCategory = "navigation"
breadcrumbData["to"] = when {
breadcrumb.data["state"] == "resumed" -> (breadcrumb.data["screen"] as? String)?.substringAfterLast('.')
"to" in breadcrumb.data -> breadcrumb.data["to"] as? String
else -> null
} ?: return null
}

breadcrumb.category == "ui.click" -> {
breadcrumbCategory = "ui.tap"
breadcrumbMessage = (
breadcrumb.data["view.id"]
?: breadcrumb.data["view.tag"]
?: breadcrumb.data["view.class"]
) as? String ?: return null
breadcrumbData.putAll(breadcrumb.data)
}

breadcrumb.type == "system" && breadcrumb.category == "network.event" -> {
breadcrumbCategory = "device.connectivity"
breadcrumbData["state"] = when {
breadcrumb.data["action"] == "NETWORK_LOST" -> "offline"
"network_type" in breadcrumb.data -> if (!(breadcrumb.data["network_type"] as? String).isNullOrEmpty()) {
breadcrumb.data["network_type"]
} else {
return null
}

else -> return null
}
}

breadcrumb.data["action"] == "BATTERY_CHANGED" -> {
breadcrumbCategory = "device.battery"
breadcrumbData.putAll(
breadcrumb.data.filterKeys { it == "level" || it == "charging" }
)
}

else -> {
breadcrumbCategory = breadcrumb.category
breadcrumbMessage = breadcrumb.message
breadcrumbLevel = breadcrumb.level
breadcrumbData.putAll(breadcrumb.data)
}
}
return if (!breadcrumbCategory.isNullOrEmpty()) {
RRWebBreadcrumbEvent().apply {
timestamp = breadcrumb.timestamp.time
breadcrumbTimestamp = breadcrumb.timestamp.time / 1000.0
breadcrumbType = "default"
category = breadcrumbCategory
message = breadcrumbMessage
level = breadcrumbLevel
data = breadcrumbData
}
} else {
null
}
}

private fun Breadcrumb.isValidForRRWebSpan(): Boolean {
return !(data["url"] as? String).isNullOrEmpty() &&
SpanDataConvention.HTTP_START_TIMESTAMP in data &&
SpanDataConvention.HTTP_END_TIMESTAMP in data
}

private fun String.snakeToCamelCase(): String {
return replace(snakecasePattern) { it.value.last().uppercase() }
}

private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent {
val breadcrumb = this
return RRWebSpanEvent().apply {
timestamp = breadcrumb.timestamp.time
op = "resource.http"
description = breadcrumb.data["url"] as String
startTimestamp =
(breadcrumb.data[SpanDataConvention.HTTP_START_TIMESTAMP] as Long) / 1000.0
endTimestamp =
(breadcrumb.data[SpanDataConvention.HTTP_END_TIMESTAMP] as Long) / 1000.0

val breadcrumbData = mutableMapOf<String, Any?>()
for ((key, value) in breadcrumb.data) {
if (key in supportedNetworkData) {
breadcrumbData[
key
.replace("content_length", "body_size")
.substringAfter(".")
.snakeToCamelCase()
] = value
}
}
data = breadcrumbData
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import android.view.MotionEvent
import io.sentry.Hint
import io.sentry.IHub
import io.sentry.Integration
import io.sentry.NoOpReplayBreadcrumbConverter
import io.sentry.ReplayBreadcrumbConverter
import io.sentry.ReplayController
import io.sentry.SentryEvent
import io.sentry.SentryIntegrationPackageStorage
Expand Down Expand Up @@ -54,6 +56,7 @@ public class ReplayIntegration(
private val isRecording = AtomicBoolean(false)
private var captureStrategy: CaptureStrategy? = null
public val replayCacheDir: File? get() = captureStrategy?.replayCacheDir
private var replayBreadcrumbConverter: ReplayBreadcrumbConverter = NoOpReplayBreadcrumbConverter.getInstance()

private lateinit var recorderConfig: ScreenshotRecorderConfig

Expand Down Expand Up @@ -158,6 +161,12 @@ public class ReplayIntegration(

override fun getReplayId(): SentryId = captureStrategy?.currentReplayId?.get() ?: SentryId.EMPTY_ID

override fun setBreadcrumbConverter(converter: ReplayBreadcrumbConverter) {
replayBreadcrumbConverter = converter
}

override fun getBreadcrumbConverter(): ReplayBreadcrumbConverter = replayBreadcrumbConverter

override fun pause() {
if (!isEnabled.get() || !isRecording.get()) {
return
Expand Down
Loading