Skip to content

Commit 0bc072b

Browse files
committed
adding rotation handling logic, piping service options through to replay GraphQL API Service
1 parent 31badab commit 0bc072b

File tree

9 files changed

+92
-68
lines changed

9 files changed

+92
-68
lines changed

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/api/Options.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import io.opentelemetry.api.common.Attributes
88
import kotlin.time.Duration
99
import kotlin.time.Duration.Companion.minutes
1010

11-
private const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318"
12-
private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
11+
const val DEFAULT_SERVICE_NAME = "observability-android"
12+
const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318"
13+
const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
1314

1415
/**
1516
* Configuration options for the Observability plugin.
@@ -31,7 +32,7 @@ private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdar
3132
* @property instrumentations List of additional instrumentations to use
3233
*/
3334
data class Options(
34-
val serviceName: String = "observability-android",
35+
val serviceName: String = DEFAULT_SERVICE_NAME,
3536
val serviceVersion: String = BuildConfig.OBSERVABILITY_SDK_VERSION,
3637
val otlpEndpoint: String = DEFAULT_OTLP_ENDPOINT,
3738
val backendUrl: String = DEFAULT_BACKEND_URL,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ class CaptureSource(
121121

122122
val rect = Rect(0, 0, decorViewWidth, decorViewHeight)
123123

124+
// protect against race condition where decor view has no size
125+
if (decorViewWidth <= 0 || decorViewHeight <= 0) {
126+
return@withContext null
127+
}
128+
124129
// TODO: O11Y-625 - optimize memory allocations
125130
// TODO: O11Y-625 - see if holding bitmap is more efficient than base64 encoding immediately after compression
126131
// TODO: O11Y-628 - use captureQuality option for scaling and adjust this bitmap accordingly, may need to investigate power of 2 rounding for performance
@@ -352,4 +357,4 @@ class CaptureSource(
352357

353358
return false
354359
}
355-
}
360+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ package com.launchdarkly.observability.replay
33
// TODO: O11Y-620 - implement full PrivacyProfiles and MaskingMatchers
44
enum class PrivacyProfile {
55
NO_MASK, STRICT
6-
}
6+
}

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

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,18 @@ private const val REPLAY_EXPORTER_NAME = "RRwebGraphQLReplayLogExporter"
2424
*/
2525
class RRwebGraphQLReplayLogExporter(
2626
val organizationVerboseId: String,
27-
val backendUrl: String
27+
val backendUrl: String,
28+
val serviceName: String,
29+
val serviceVersion: String,
2830
) : LogRecordExporter {
2931
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
3032

3133
private var graphqlClient: GraphQLClient = GraphQLClient(backendUrl)
32-
private var replayApiService: SessionReplayApiService = SessionReplayApiService(graphqlClient)
34+
private var replayApiService: SessionReplayApiService = SessionReplayApiService(
35+
graphqlClient = graphqlClient,
36+
serviceName = serviceName,
37+
serviceVersion = serviceVersion,
38+
)
3339

3440
// TODO: O11Y-624 - need to implement sid, payloadId reset when multiple sessions occur in one application process lifecycle.
3541
private var sidCounter = 0
@@ -44,22 +50,24 @@ class RRwebGraphQLReplayLogExporter(
4450
coroutineScope.launch {
4551
try {
4652
var allSuccessful = true
47-
53+
4854
for (log in logs) {
4955
val capture = extractCaptureFromLog(log)
5056
if (capture != null) {
5157
// TODO: O11Y-624 - investigate if there is a size limit on the push that is imposed server side.
52-
val success = if (!capture.session.equals(lastSessionId)) {
53-
sendCaptureInitial(capture)
54-
} else {
55-
sendCaptureIncremental(capture)
56-
}
58+
val success =
59+
if (!capture.session.equals(lastSessionId) || lastSentWidth != capture.origWidth || lastSentHeight != capture.origHeight) {
60+
// we need to send a full capture if the session id changes or there is a resize/orientation change
61+
sendCaptureFull(capture)
62+
} else {
63+
sendCaptureIncremental(capture)
64+
}
5765
if (!success) {
5866
allSuccessful = false
5967
}
6068
}
6169
}
62-
70+
6371
if (allSuccessful) {
6472
resultCode.succeed()
6573
} else {
@@ -71,7 +79,7 @@ class RRwebGraphQLReplayLogExporter(
7179
resultCode.fail()
7280
}
7381
}
74-
82+
7583
return resultCode
7684
}
7785

@@ -85,25 +93,25 @@ class RRwebGraphQLReplayLogExporter(
8593
TODO("Not yet implemented")
8694
}
8795

88-
fun nextSid() : Int {
96+
fun nextSid(): Int {
8997
sidCounter++;
9098
return sidCounter
9199
}
92100

93-
fun nextPayloadId() : Int {
101+
fun nextPayloadId(): Int {
94102
payloadIdCounter++;
95103
return payloadIdCounter
96104
}
97105

98106
// Returns null if unable to extract a valid capture from the log record
99-
private fun extractCaptureFromLog(log: LogRecordData) : Capture? {
107+
private fun extractCaptureFromLog(log: LogRecordData): Capture? {
100108
val attributes = log.attributes
101109
val eventDomain = attributes.get(AttributeKey.stringKey("event.domain"))
102110
val imageWidth = attributes.get(AttributeKey.longKey("image.width"))
103111
val imageHeight = attributes.get(AttributeKey.longKey("image.height"))
104112
val imageData = attributes.get(AttributeKey.stringKey("image.data"))
105113
val sessionId = attributes.get(AttributeKey.stringKey("session.id"))
106-
114+
107115
// Return null if any required attribute is missing
108116
if (eventDomain != "media" || imageWidth == null || imageHeight == null || imageData == null || sessionId == null) {
109117
return null
@@ -119,13 +127,13 @@ class RRwebGraphQLReplayLogExporter(
119127
}
120128

121129
/**
122-
* Sends an incremental capture. Used after [sendCaptureInitial] has already been called for a previous capture in the same session.
130+
* Sends an incremental capture. Used after [sendCaptureFull] has already been called for a previous capture in the same session.
123131
*
124132
* @param capture the capture to be sent
125133
*/
126134
suspend fun sendCaptureIncremental(capture: Capture): Boolean = withContext(Dispatchers.IO) {
127135
try {
128-
val eventsBatch1 = mutableListOf<Event>()
136+
val eventsBatch = mutableListOf<Event>()
129137
val timestamp = System.currentTimeMillis()
130138

131139
// TODO: O11Y-625 - optimize JSON usage for performance since this region of code is essentially static
@@ -137,13 +145,11 @@ class RRwebGraphQLReplayLogExporter(
137145
Json.parseToJsonElement("""{"source":9,"id":6,"type":0,"commands":[{"property":"clearRect","args":[0,0,${capture.origWidth},${capture.origHeight}]},{"property":"drawImage","args":[{"rr_type":"ImageBitmap","args":[{"rr_type":"Blob","data":[{"rr_type":"ArrayBuffer","base64":"${capture.imageBase64}"}],"type":"image/jpeg"}]},0,0,${capture.origWidth},${capture.origHeight}]}]}""")
138146
)
139147
)
140-
eventsBatch1.add(incrementalEvent)
141-
142-
// TODO: add ViewPort event if resolution changes
148+
eventsBatch.add(incrementalEvent)
143149

144150
// TODO: O11Y-629 - remove this spoofed mouse interaction when proper user interaction is instrumented
145151
// This spoofed mouse interaction is necessary to make the session look like it had activity
146-
eventsBatch1.add(
152+
eventsBatch.add(
147153
Event(
148154
type = EventType.INCREMENTAL_SNAPSHOT,
149155
timestamp = timestamp,
@@ -154,21 +160,31 @@ class RRwebGraphQLReplayLogExporter(
154160
)
155161
)
156162

157-
replayApiService.pushPayload(capture.session, "${nextPayloadId()}", eventsBatch1)
163+
// record last sent state
164+
lastSessionId = capture.session
165+
lastSentWidth = capture.origWidth
166+
lastSentHeight = capture.origHeight
167+
168+
replayApiService.pushPayload(capture.session, "${nextPayloadId()}", eventsBatch)
158169
true
159170
} catch (e: Exception) {
160171
// TODO: O11Y-627 - pass in logger to implementation and use here
161-
Log.e(REPLAY_EXPORTER_NAME, "Error sending incremental capture for session: ${e.message}", e)
172+
Log.e(
173+
REPLAY_EXPORTER_NAME,
174+
"Error sending incremental capture for session: ${e.message}",
175+
e
176+
)
162177
false
163178
}
164179
}
165180

166181
/**
167-
* Sends an initial capture. Used after [sendCaptureInitial] has already been called for a previous capture in the same session.
182+
* Sends a full capture. May be invoked multiple times for a single session if a substantial
183+
* change occurs requiring a full capture to be sent.
168184
*
169185
* @param capture the capture to be sent
170186
*/
171-
suspend fun sendCaptureInitial(capture: Capture): Boolean = withContext(Dispatchers.IO) {
187+
suspend fun sendCaptureFull(capture: Capture): Boolean = withContext(Dispatchers.IO) {
172188
try {
173189
replayApiService.initializeReplaySession(organizationVerboseId, capture.session)
174190
replayApiService.identifyReplaySession(capture.session)
@@ -256,18 +272,23 @@ class RRwebGraphQLReplayLogExporter(
256272
)
257273
eventBatch.add(viewportEvent)
258274

259-
// TODO: O11Y-624 - double check error case handling, may need to add retries per api service request, should subsequent requests wait for previous requests to succeed?
275+
// record last sent state
260276
lastSessionId = capture.session
261277
lastSentWidth = capture.origWidth
262278
lastSentHeight = capture.origHeight
263279

280+
// TODO: O11Y-624 - double check error case handling, may need to add retries per api service request, should subsequent requests wait for previous requests to succeed?
264281
replayApiService.pushPayload(capture.session, "${nextPayloadId()}", eventBatch)
265282

266283
true
267284
} catch (e: Exception) {
268285
// TODO: O11Y-627 - pass in logger to implementation and use here
269-
Log.e(REPLAY_EXPORTER_NAME, "Error sending initial capture for session: ${e.message}", e)
286+
Log.e(
287+
REPLAY_EXPORTER_NAME,
288+
"Error sending initial capture for session: ${e.message}",
289+
e
290+
)
270291
false
271292
}
272293
}
273-
}
294+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ class ReplayInstrumentation(
150150
override fun getLogRecordProcessor(credential: String): LogRecordProcessor {
151151
val exporter = RRwebGraphQLReplayLogExporter(
152152
organizationVerboseId = credential, // the SDK credential is used as the organization ID intentionally
153-
backendUrl = options.backendUrl
153+
backendUrl = options.backendUrl,
154+
serviceName = options.serviceName,
155+
serviceVersion = options.serviceVersion,
154156
)
155157

156158
return BatchLogRecordProcessor.builder(exporter)
@@ -160,4 +162,4 @@ class ReplayInstrumentation(
160162
.setMaxExportBatchSize(BATCH_MAX_EXPORT_SIZE)
161163
.build()
162164
}
163-
}
165+
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.launchdarkly.observability.replay
22

3-
private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
3+
import com.launchdarkly.observability.BuildConfig
4+
import com.launchdarkly.observability.api.DEFAULT_BACKEND_URL
45

56
/**
67
* Options for the [ReplayInstrumentation]
@@ -11,9 +12,11 @@ private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdar
1112
* @property capturePeriodMillis period between captures
1213
*/
1314
data class ReplayOptions(
15+
val serviceName: String = "observability-android",
16+
val serviceVersion: String = BuildConfig.OBSERVABILITY_SDK_VERSION,
1417
val backendUrl: String = DEFAULT_BACKEND_URL,
1518
val debug: Boolean = false,
1619
val privacyProfile: PrivacyProfile = PrivacyProfile.STRICT,
1720
val capturePeriodMillis: Long = 1000, // defaults to ever 1 second
1821
// TODO O11Y-623 - Add storage options
19-
)
22+
)

0 commit comments

Comments
 (0)