Skip to content

Commit

Permalink
[SR] Capture breadcrumbs (temporary)
Browse files Browse the repository at this point in the history
  • Loading branch information
romtsn authored Apr 25, 2024
2 parents 12c0eb7 + 4c7d1a0 commit db66737
Show file tree
Hide file tree
Showing 7 changed files with 488 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.submitSafely
import io.sentry.protocol.SentryId
import io.sentry.rrweb.RRWebBreadcrumbEvent
import io.sentry.rrweb.RRWebEvent
import io.sentry.rrweb.RRWebMetaEvent
import io.sentry.rrweb.RRWebVideoEvent
import io.sentry.transport.ICurrentDateProvider
Expand All @@ -28,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference

internal abstract class BaseCaptureStrategy(
private val options: SentryOptions,
private val hub: IHub?,
private val dateProvider: ICurrentDateProvider,
protected var recorderConfig: ScreenshotRecorderConfig,
executor: ScheduledExecutorService? = null
Expand Down Expand Up @@ -134,41 +137,107 @@ internal abstract class BaseCaptureStrategy(
duration: Long,
replayType: ReplayType
): ReplaySegment {
val endTimestamp = DateUtils.getDateTime(segmentTimestamp.time + duration)
val replay = SentryReplayEvent().apply {
eventId = currentReplayId
replayId = currentReplayId
this.segmentId = segmentId
this.timestamp = DateUtils.getDateTime(segmentTimestamp.time + duration)
this.timestamp = endTimestamp
replayStartTimestamp = segmentTimestamp
this.replayType = replayType
videoFile = video
}

val recording = ReplayRecording().apply {
val recordingPayload = mutableListOf<RRWebEvent>()
recordingPayload += RRWebMetaEvent().apply {
this.timestamp = segmentTimestamp.time
this.height = height
this.width = width
}
recordingPayload += RRWebVideoEvent().apply {
this.timestamp = segmentTimestamp.time
this.segmentId = segmentId
payload = listOf(
RRWebMetaEvent().apply {
this.timestamp = segmentTimestamp.time
this.height = height
this.width = width
},
RRWebVideoEvent().apply {
this.timestamp = segmentTimestamp.time
this.segmentId = segmentId
this.durationMs = duration
this.frameCount = frameCount
size = video.length()
frameRate = recorderConfig.frameRate
this.height = height
this.width = width
// TODO: support non-fullscreen windows later
left = 0
top = 0
this.durationMs = duration
this.frameCount = frameCount
size = video.length()
frameRate = recorderConfig.frameRate
this.height = height
this.width = width
// TODO: support non-fullscreen windows later
left = 0
top = 0
}

hub?.configureScope { scope ->
scope.breadcrumbs.forEach { breadcrumb ->
if (breadcrumb.timestamp.after(segmentTimestamp) &&
breadcrumb.timestamp.before(endTimestamp)
) {
// TODO: rework this later when aligned with iOS and frontend
var breadcrumbMessage: String? = null
val breadcrumbCategory: String?
val breadcrumbData = mutableMapOf<String, Any?>()
when {
breadcrumb.category == "http" -> return@forEach

breadcrumb.category == "device.orientation" -> {
breadcrumbCategory = breadcrumb.category!!
breadcrumbMessage = breadcrumb.data["position"] as? String ?: ""
}

breadcrumb.type == "navigation" -> {
breadcrumbCategory = "navigation"
breadcrumbData["to"] = when {
breadcrumb.data["state"] == "resumed" -> breadcrumb.data["screen"] as? String
breadcrumb.category == "app.lifecycle" -> breadcrumb.data["state"] as? String
"to" in breadcrumb.data -> breadcrumb.data["to"] as? String
else -> return@forEach
} ?: return@forEach
}

breadcrumb.category in setOf("ui.click", "ui.scroll", "ui.swipe") -> {
breadcrumbCategory = breadcrumb.category!!
breadcrumbMessage = (
breadcrumb.data["view.id"]
?: breadcrumb.data["view.class"]
?: breadcrumb.data["view.tag"]
) as? String ?: ""
}

breadcrumb.type == "system" -> {
breadcrumbCategory = breadcrumb.type!!
breadcrumbMessage = breadcrumb.data.entries.joinToString() as? String ?: ""
}

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

return ReplaySegment.Created(videoDuration = duration, replay = replay, recording = recording)
val recording = ReplayRecording().apply {
this.segmentId = segmentId
payload = recordingPayload.sortedBy { it.timestamp }
}

return ReplaySegment.Created(
videoDuration = duration,
replay = replay,
recording = recording
)
}

override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal class BufferCaptureStrategy(
private val dateProvider: ICurrentDateProvider,
recorderConfig: ScreenshotRecorderConfig,
private val random: SecureRandom
) : BaseCaptureStrategy(options, dateProvider, recorderConfig) {
) : BaseCaptureStrategy(options, hub, dateProvider, recorderConfig) {

private val bufferedSegments = mutableListOf<ReplaySegment.Created>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal class SessionCaptureStrategy(
private val dateProvider: ICurrentDateProvider,
recorderConfig: ScreenshotRecorderConfig,
executor: ScheduledExecutorService? = null
) : BaseCaptureStrategy(options, dateProvider, recorderConfig, executor) {
) : BaseCaptureStrategy(options, hub, dateProvider, recorderConfig, executor) {

internal companion object {
private const val TAG = "SessionCaptureStrategy"
Expand Down
40 changes: 40 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -5035,6 +5035,46 @@ public final class io/sentry/protocol/ViewHierarchyNode$JsonKeys {
public fun <init> ()V
}

public final class io/sentry/rrweb/RRWebBreadcrumbEvent : io/sentry/rrweb/RRWebEvent, io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public static final field EVENT_TAG Ljava/lang/String;
public fun <init> ()V
public fun getBreadcrumbTimestamp ()D
public fun getBreadcrumbType ()Ljava/lang/String;
public fun getCategory ()Ljava/lang/String;
public fun getData ()Ljava/util/Map;
public fun getDataUnknown ()Ljava/util/Map;
public fun getMessage ()Ljava/lang/String;
public fun getPayloadUnknown ()Ljava/util/Map;
public fun getTag ()Ljava/lang/String;
public fun getUnknown ()Ljava/util/Map;
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
public fun setBreadcrumbTimestamp (D)V
public fun setBreadcrumbType (Ljava/lang/String;)V
public fun setCategory (Ljava/lang/String;)V
public fun setData (Ljava/util/Map;)V
public fun setDataUnknown (Ljava/util/Map;)V
public fun setMessage (Ljava/lang/String;)V
public fun setPayloadUnknown (Ljava/util/Map;)V
public fun setTag (Ljava/lang/String;)V
public fun setUnknown (Ljava/util/Map;)V
}

public final class io/sentry/rrweb/RRWebBreadcrumbEvent$Deserializer : io/sentry/JsonDeserializer {
public fun <init> ()V
public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/rrweb/RRWebBreadcrumbEvent;
public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object;
}

public final class io/sentry/rrweb/RRWebBreadcrumbEvent$JsonKeys {
public static final field CATEGORY Ljava/lang/String;
public static final field DATA Ljava/lang/String;
public static final field MESSAGE Ljava/lang/String;
public static final field PAYLOAD Ljava/lang/String;
public static final field TIMESTAMP Ljava/lang/String;
public static final field TYPE Ljava/lang/String;
public fun <init> ()V
}

public abstract class io/sentry/rrweb/RRWebEvent {
protected fun <init> ()V
protected fun <init> (Lio/sentry/rrweb/RRWebEventType;)V
Expand Down
Loading

0 comments on commit db66737

Please sign in to comment.