Skip to content

Commit 4ba5124

Browse files
authored
feat: Android observability plugin session replay support (#268)
## Summary Adds initial implementation of Android observability plugin session replay support. Still has many remaining TODOs, some of which will be given new stories for fleshing out specific handling/behaviors. ## How did you test this change? Unit tests on instrumentation manager. There is a later story in the epic to add e2e instrumented tests. ## Are there any deployment considerations? This is only for alpha consumption and no promises related to stability of code can be made at this time. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces session replay capture/export via RRWeb GraphQL, adds pluggable instrumentations with a routing log processor, updates GraphQL var handling and TelemetryInspector. > > - **SDK/Core**: > - **Session Replay**: New replay pipeline with `ReplayInstrumentation`, `CaptureSource`, `RRwebGraphQLReplayLogExporter`, `SessionReplayApiService`, protocol models (`ReplaySessionProtocol.kt`), and GraphQL ops (`InitializeReplaySession.graphql`, `IdentifyReplaySession.graphql`, `PushPayload.graphql`). > - **Options**: Add `instrumentations: List<LDExtendedInstrumentation>`; expose `DEFAULT_SERVICE_NAME`, `DEFAULT_OTLP_ENDPOINT`, `DEFAULT_BACKEND_URL`. > - **InstrumentationManager**: Support external instrumentations; add `createLoggerProcessor` using `RoutingLogRecordProcessor`; integrate `TelemetryInspector` exporters (lazy); minor refactors to exporters/processors. > - **Logging**: Add `RoutingLogRecordProcessor` and `NoopLogRecordProcessor` for scope-based log routing. > - **GraphQL**: Change variables to `JsonElement`; update `SamplingApiService` accordingly. > - **Build**: Add Compose UI deps for capture. > - **Example App**: > - Include `ReplayInstrumentation` in `BaseApplication` plugin options. > - **Tests**: > - New tests for logger processor routing and replay exporter; update GraphQL/Sampling tests for `JsonPrimitive`; adjust E2E disabling tests to not depend on null exporters. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d52ac80. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 38b4a84 commit 4ba5124

27 files changed

+2341
-96
lines changed

e2e/android/app/src/main/java/com/example/androidobservability/BaseApplication.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.launchdarkly.sdk.android.Components
99
import com.launchdarkly.sdk.android.LDClient
1010
import com.launchdarkly.sdk.android.LDConfig
1111
import com.launchdarkly.observability.plugin.Observability
12+
import com.launchdarkly.observability.replay.ReplayInstrumentation
1213
import com.launchdarkly.sdk.android.LDAndroidLogging
1314
import com.launchdarkly.sdk.android.integrations.Plugin
1415
import io.opentelemetry.api.common.AttributeKey
@@ -29,6 +30,10 @@ open class BaseApplication : Application() {
2930
),
3031
debug = true,
3132
logAdapter = LDAndroidLogging.adapter(),
33+
// TODO: consider these being factories so that the obs plugin can pass instantiation data, log adapter
34+
instrumentations = listOf(
35+
ReplayInstrumentation()
36+
),
3237
)
3338

3439
var telemetryInspector: TelemetryInspector? = null

e2e/android/app/src/test/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@ package com.example.androidobservability
22

33
import android.app.Application
44
import androidx.test.core.app.ApplicationProvider
5+
import com.example.androidobservability.TestUtils.TelemetryType
56
import com.example.androidobservability.TestUtils.waitForTelemetryData
7+
import com.launchdarkly.observability.api.Options
68
import com.launchdarkly.observability.interfaces.Metric
79
import com.launchdarkly.observability.sdk.LDObserve
810
import io.opentelemetry.api.common.AttributeKey
911
import io.opentelemetry.api.common.Attributes
1012
import io.opentelemetry.api.logs.Severity
11-
import com.example.androidobservability.TestUtils.TelemetryType
12-
import com.launchdarkly.observability.api.Options
1313
import junit.framework.TestCase.assertEquals
1414
import junit.framework.TestCase.assertFalse
1515
import junit.framework.TestCase.assertNotNull
16-
import junit.framework.TestCase.assertNull
1716
import junit.framework.TestCase.assertTrue
1817
import org.junit.Test
1918
import org.junit.runner.RunWith
@@ -96,7 +95,6 @@ class DisablingConfigOptionsE2ETest {
9695
LDObserve.flush()
9796
waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.METRICS)
9897

99-
assertNull(application.telemetryInspector?.metricExporter)
10098
assertFalse(requestsContainsUrl(metricsUrl))
10199
}
102100

@@ -110,7 +108,6 @@ class DisablingConfigOptionsE2ETest {
110108
LDObserve.flush()
111109
waitForTelemetryData(telemetryInspector = application.telemetryInspector, telemetryType = TelemetryType.METRICS)
112110

113-
assertNotNull(application.telemetryInspector?.metricExporter)
114111
assertTrue(requestsContainsUrl(metricsUrl))
115112
}
116113

sdk/@launchdarkly/observability-android/lib/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ dependencies {
5555
// Android crash instrumentation
5656
implementation("io.opentelemetry.android.instrumentation:crash:0.11.0-alpha")
5757

58+
// TODO: O11Y-626 - move replay instrumentation and associated compose dependencies into dedicated package
59+
// Compose dependencies for capture functionality
60+
implementation("androidx.compose.ui:ui:1.7.5")
61+
implementation("androidx.compose.ui:ui-tooling:1.7.5")
62+
5863
// Use JUnit Jupiter for testing.
5964
testImplementation(platform("org.junit:junit-bom:5.13.4"))
6065
testImplementation("org.junit.jupiter:junit-jupiter")

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package com.launchdarkly.observability.api
22

33
import com.launchdarkly.logging.LDLogAdapter
44
import com.launchdarkly.observability.BuildConfig
5+
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
56
import com.launchdarkly.sdk.android.LDTimberLogging
67
import io.opentelemetry.api.common.Attributes
78
import kotlin.time.Duration
89
import kotlin.time.Duration.Companion.minutes
910

10-
private const val DEFAULT_OTLP_ENDPOINT = "https://otel.observability.app.launchdarkly.com:4318"
11-
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"
1214

1315
/**
1416
* Configuration options for the Observability plugin.
@@ -27,21 +29,23 @@ private const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdar
2729
* @property disableMetrics Disables metrics if true. Defaults to false.
2830
* @property logAdapter The log adapter to use. Defaults to using the LaunchDarkly SDK's LDTimberLogging.adapter(). Use LDAndroidLogging.adapter() to use the Android logging adapter.
2931
* @property loggerName The name of the logger to use. Defaults to "LaunchDarklyObservabilityPlugin".
32+
* @property instrumentations List of additional instrumentations to use
3033
*/
3134
data class Options(
32-
val serviceName: String = "observability-android",
35+
val serviceName: String = DEFAULT_SERVICE_NAME,
3336
val serviceVersion: String = BuildConfig.OBSERVABILITY_SDK_VERSION,
3437
val otlpEndpoint: String = DEFAULT_OTLP_ENDPOINT,
3538
val backendUrl: String = DEFAULT_BACKEND_URL,
3639
val resourceAttributes: Attributes = Attributes.empty(),
3740
val customHeaders: Map<String, String> = emptyMap(),
3841
val sessionBackgroundTimeout: Duration = 15.minutes,
3942
val debug: Boolean = false,
40-
// TODO O11Y-398: implement disable config options after all other instrumentations are implemented
4143
val disableErrorTracking: Boolean = false,
4244
val disableLogs: Boolean = false,
4345
val disableTraces: Boolean = false,
4446
val disableMetrics: Boolean = false,
4547
val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // this follows the LaunchDarkly SDK's default log adapter
46-
val loggerName: String = "LaunchDarklyObservabilityPlugin"
48+
val loggerName: String = "LaunchDarklyObservabilityPlugin",
49+
// TODO: update this to provide a list of factories instead of instances
50+
val instrumentations: List<LDExtendedInstrumentation> = emptyList()
4751
)

0 commit comments

Comments
 (0)