Skip to content

Commit ad6d0aa

Browse files
feat: Android SR use Jpeg 0.3 quality (#417)
WEBP is 4 times more expensive compare Jpeg. Need investigate why? (cherry picked from commit 0b3ad7d) ## Summary <!-- Ideally, there is an attached GitHub issue that will describe the "why". If relevant, use this section to call out any additional information you'd like to _highlight_ to the reviewer. --> ## How did you test this change? <!-- Frontend - Leave a screencast or a screenshot to visually describe the changes. --> ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the image encoding format and MIME type used in replay payloads, which could affect replay rendering/compatibility and payload size/quality characteristics. > > **Overview** > Switches Android session replay tile/image encoding from per-frame `Webp` to a shared `ExportFrame.DEFAULT_EXPORT_FORMAT` (now `Jpeg(quality=0.3)`), and removes the `format` field from `ExportFrame` so capture/export code no longer carries format metadata per frame. > > Updates `RRWebEventGenerator` to build image data URLs using the default format’s MIME type, and adjusts/extends tests to validate JPEG MIME output for the convenience `ExportFrame` constructor. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c29aed9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 323f0b5 commit ad6d0aa

File tree

4 files changed

+43
-11
lines changed

4 files changed

+43
-11
lines changed

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/ExportDiffManager.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ExportDiffManager(
1313
private val currentImages = mutableListOf<ExportFrame.RemoveImage>()
1414
private val currentImagesIndex = mutableMapOf<ImageSignature, Int>()
1515
private val lock = Any()
16-
private val format = ExportFrame.ExportFormat.Webp(quality = 30) //ExportFrame.ExportFormat.Jpeg(quality = 0.3f)
16+
private val format = ExportFrame.DEFAULT_EXPORT_FORMAT
1717

1818
private var keyFrameId = 0
1919

@@ -98,7 +98,6 @@ class ExportDiffManager(
9898
removeImages = removes,
9999
originalSize = tiledFrame.originalSize,
100100
scale = tiledFrame.scale,
101-
format = format,
102101
timestamp = tiledFrame.timestamp,
103102
orientation = tiledFrame.orientation,
104103
isKeyframe = tiledFrame.isKeyframe,

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/capture/ExportFrame.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ data class ExportFrame(
99
val removeImages: List<RemoveImage>?,
1010
val originalSize: IntSize,
1111
val scale: Float,
12-
val format: ExportFormat,
1312
val timestamp: Long,
1413
val orientation: Int,
1514
val isKeyframe: Boolean,
1615
val imageSignature: ImageSignature?,
1716
val session: String
1817
){
18+
companion object {
19+
val DEFAULT_EXPORT_FORMAT: ExportFormat = ExportFormat.Jpeg(quality = 0.3f)
20+
}
21+
1922
constructor(
2023
imageBase64: String,
2124
origHeight: Int,
@@ -34,7 +37,6 @@ data class ExportFrame(
3437
removeImages = null,
3538
originalSize = IntSize(width = origWidth, height = origHeight),
3639
scale = 1f,
37-
format = ExportFormat.Webp(quality = 30),
3840
timestamp = timestamp,
3941
orientation = 0,
4042
isKeyframe = true,

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/exporter/RRWebEventGenerator.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,17 @@ class RRWebEventGenerator(
7979
return lastNodeId
8080
}
8181

82-
private fun imageMimeType(format: ExportFrame.ExportFormat): String =
83-
when (format) {
82+
private fun imageMimeType(): String =
83+
when (ExportFrame.DEFAULT_EXPORT_FORMAT) {
8484
ExportFrame.ExportFormat.Png -> "image/png"
8585
is ExportFrame.ExportFormat.Jpeg -> "image/jpeg"
8686
is ExportFrame.ExportFormat.Webp -> "image/webp"
8787
}
8888

89-
private fun tileNode(exportFrame: ExportFrame, image: ExportFrame.AddImage): Pair<EventNode, Int> {
89+
private fun tileNode(image: ExportFrame.AddImage): Pair<EventNode, Int> {
9090
val tileCanvasId = nextNodeId()
9191
image.imageSignature?.let { nodeIds[it] = tileCanvasId }
92-
val dataUrl = "data:${imageMimeType(exportFrame.format)};base64,${image.imageBase64}"
92+
val dataUrl = "data:${imageMimeType()};base64,${image.imageBase64}"
9393
val node = EventNode(
9494
id = tileCanvasId,
9595
type = NodeType.ELEMENT,
@@ -123,7 +123,7 @@ class RRWebEventGenerator(
123123
}
124124

125125
val adds = exportFrame.addImages.map { image ->
126-
val (node, canvasSize) = tileNode(exportFrame, image)
126+
val (node, canvasSize) = tileNode(image)
127127
totalCanvasSize += canvasSize
128128
Addition(parentId = bodyId, nextId = null, node = node)
129129
}
@@ -213,7 +213,7 @@ class RRWebEventGenerator(
213213
val headNodeId = nextNodeId()
214214
val currentBodyNodeId = nextNodeId()
215215
val tileNodes = exportFrame.addImages.map { image ->
216-
val (node, canvasSize) = tileNode(exportFrame, image)
216+
val (node, canvasSize) = tileNode(image)
217217
totalCanvasSize += canvasSize
218218
node
219219
}

sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/replay/exporter/RRWebEventGeneratorTest.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.launchdarkly.observability.replay.exporter
33
import com.launchdarkly.observability.replay.Event
44
import com.launchdarkly.observability.replay.EventData
55
import com.launchdarkly.observability.replay.EventDataUnion
6+
import com.launchdarkly.observability.replay.EventNode
67
import com.launchdarkly.observability.replay.EventType
78
import com.launchdarkly.observability.replay.capture.ExportFrame
89
import com.launchdarkly.observability.replay.capture.ImageSignature
@@ -14,6 +15,16 @@ import org.junit.jupiter.api.Assertions.assertTrue
1415
import org.junit.jupiter.api.Test
1516

1617
class RRWebEventGeneratorTest {
18+
@Test
19+
fun `convenience export frame uses jpeg mime type`() {
20+
val generator = RRWebEventGenerator(canvasDrawEntourage = 1)
21+
val exportFrame = ExportFrame("AQ==", 88, 120, 1L, "session")
22+
23+
val events = generator.generateCaptureFullEvents(exportFrame)
24+
val src = firstImageSrc(events)
25+
26+
assertTrue(src.startsWith("data:image/jpeg;base64,"))
27+
}
1728

1829
@Test
1930
fun `keyframe incremental resolves removes before map reset`() {
@@ -134,7 +145,6 @@ class RRWebEventGeneratorTest {
134145
removeImages = removeImages,
135146
originalSize = IntSize(width = 120, height = 88),
136147
scale = 1f,
137-
format = ExportFrame.ExportFormat.Webp(quality = 30),
138148
timestamp = timestamp,
139149
orientation = 0,
140150
isKeyframe = isKeyframe,
@@ -159,4 +169,25 @@ class RRWebEventGeneratorTest {
159169
val data = event.data as EventDataUnion.StandardEventData
160170
return data.data
161171
}
172+
173+
private fun firstImageSrc(events: List<Event>): String {
174+
val fullSnapshot = events.first { it.type == EventType.FULL_SNAPSHOT }
175+
val data = (fullSnapshot.data as EventDataUnion.StandardEventData).data
176+
val root = data.node ?: error("FULL_SNAPSHOT should include a root node")
177+
val imageNode = firstNodeWithTag(root, "img") ?: error("FULL_SNAPSHOT should include an image node")
178+
return imageNode.attributes?.get("src") ?: error("Image node should include src")
179+
}
180+
181+
private fun firstNodeWithTag(node: EventNode, tagName: String): EventNode? {
182+
if (node.tagName == tagName) {
183+
return node
184+
}
185+
for (child in node.childNodes) {
186+
val match = firstNodeWithTag(child, tagName)
187+
if (match != null) {
188+
return match
189+
}
190+
}
191+
return null
192+
}
162193
}

0 commit comments

Comments
 (0)