Skip to content

Commit 63dcf12

Browse files
authored
refactor: OY11-846 - Add Session Replay plugin (#313)
## Summary - Introduces the Session Replay plugin for the LaunchDarkly Android SDK, allowing session recording as an extension to Observability. - Refactors plugin and instrumentation management by adding PluginManager and InstrumentationContributor, removes direct instrumentation from Options, and updates ObservabilityClient and InstrumentationManager to support plugin-contributed instrumentations. - Updates documentation and sample usage to reflect the new plugin architecture. Also bumps launchdarkly-android-client-sdk dependency to 5.9.2. ## How did you test this change? - Unit tests added <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Session Replay plugin and refactors Observability to install plugin-contributed instrumentations; updates docs, sample app, and bumps dependencies. > > - **SDK/Plugins**: > - **Session Replay**: New `SessionReplay` plugin with `ReplayInstrumentation` and `ReplayOptions`; integrates via `InstrumentationContributor`. > - **Observability**: Defers initialization to `onPluginsReady`, exposes `LDObserve.context`, and installs instrumentations from `InstrumentationContributorManager`. > - **API Changes**: Remove `Options.instrumentations`; add `ObservabilityContext` for cross-plugin sharing. > - **Instrumentation Routing**: `ObservabilityClient`/`InstrumentationManager` accept `List<LDExtendedInstrumentation>` and route logs via `RoutingLogRecordProcessor`. > - **Infrastructure**: > - Add `InstrumentationContributor` interface and `InstrumentationContributorManager` for per-`LDClient` registration. > - **Docs/Sample**: > - README: Document enabling Session Replay and masking APIs. > - E2E app: Adds `SessionReplay` after `Observability` in `LDConfig` setup. > - **Dependencies**: > - Bump `launchdarkly-android-client-sdk` to `5.10.0`; upgrade Kotlin to `2.2.0`. > - **Tests**: > - New unit tests for instrumentation routing, contributor manager, and session replay registration/creation. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4bb166e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 848d6c9 commit 63dcf12

File tree

20 files changed

+466
-122
lines changed

20 files changed

+466
-122
lines changed

e2e/android/app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ dependencies {
5454
// Uncomment to use the publicly released version (note this may be behind branch/main)
5555
// implementation("com.launchdarkly:launchdarkly-observability-android:0.2.0")
5656

57-
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
57+
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.10.0")
5858

5959
implementation("io.opentelemetry:opentelemetry-api:1.51.0")
6060
implementation("io.opentelemetry:opentelemetry-sdk:1.51.0")

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

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ package com.example.androidobservability
33
import android.app.Application
44
import com.launchdarkly.observability.api.Options
55
import com.launchdarkly.observability.client.TelemetryInspector
6+
import com.launchdarkly.observability.plugin.Observability
7+
import com.launchdarkly.observability.replay.PrivacyProfile
8+
import com.launchdarkly.observability.replay.ReplayOptions
9+
import com.launchdarkly.observability.replay.SessionReplay
610
import com.launchdarkly.sdk.ContextKind
711
import com.launchdarkly.sdk.LDContext
812
import com.launchdarkly.sdk.android.Components
13+
import com.launchdarkly.sdk.android.LDAndroidLogging
914
import com.launchdarkly.sdk.android.LDClient
1015
import com.launchdarkly.sdk.android.LDConfig
11-
import com.launchdarkly.observability.plugin.Observability
12-
import com.launchdarkly.observability.replay.PrivacyProfile
13-
import com.launchdarkly.observability.replay.ReplayInstrumentation
14-
import com.launchdarkly.observability.replay.ReplayOptions
15-
import com.launchdarkly.sdk.android.LDAndroidLogging
16-
import com.launchdarkly.sdk.android.integrations.Plugin
1716
import io.opentelemetry.api.common.AttributeKey
1817
import io.opentelemetry.api.common.Attributes
19-
import java.util.Collections
2018

2119
open class BaseApplication : Application() {
2220

@@ -32,14 +30,6 @@ open class BaseApplication : Application() {
3230
),
3331
debug = true,
3432
logAdapter = LDAndroidLogging.adapter(),
35-
// TODO: consider these being factories so that the obs plugin can pass instantiation data, log adapter
36-
instrumentations = listOf(
37-
ReplayInstrumentation(
38-
options = ReplayOptions(
39-
privacyProfile = PrivacyProfile(maskText = false)
40-
)
41-
)
42-
),
4333
)
4434

4535
var telemetryInspector: TelemetryInspector? = null
@@ -52,6 +42,12 @@ open class BaseApplication : Application() {
5242
options = testUrl?.let { pluginOptions.copy(backendUrl = it, otlpEndpoint = it) } ?: pluginOptions
5343
)
5444

45+
val sessionReplayPlugin = SessionReplay(
46+
options = ReplayOptions(
47+
privacyProfile = PrivacyProfile(maskText = false)
48+
)
49+
)
50+
5551
// Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly mobile key found on the LaunchDarkly
5652
// dashboard in the start guide.
5753
// If you want to disable the Auto EnvironmentAttributes functionality.
@@ -60,7 +56,10 @@ open class BaseApplication : Application() {
6056
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
6157
.plugins(
6258
Components.plugins().setPlugins(
63-
Collections.singletonList<Plugin>(observabilityPlugin)
59+
listOf(
60+
observabilityPlugin,
61+
sessionReplayPlugin
62+
)
6463
)
6564
)
6665
.build()

e2e/android/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
agp = "8.9.2"
3-
kotlin = "2.0.21"
3+
kotlin = "2.2.0"
44
coreKtx = "1.16.0"
55
junit = "4.13.2"
66
junitVersion = "1.2.1"

sdk/@launchdarkly/observability-android/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,31 @@ span.end()
169169

170170
### Session Replay
171171

172+
#### Enable Session Replay
173+
174+
Add the Session Replay plugin **after** Observability when configuring the LaunchDarkly SDK:
175+
176+
```kotlin
177+
import com.launchdarkly.observability.plugin.Observability
178+
import com.launchdarkly.observability.replay.SessionReplay
179+
180+
val ldConfig = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled)
181+
.mobileKey("your-mobile-key")
182+
.plugins(
183+
Components.plugins().setPlugins(
184+
listOf(
185+
Observability(this@MyApplication, "your-mobile-key"),
186+
SessionReplay() // depends on Observability being present first
187+
)
188+
)
189+
)
190+
.build()
191+
```
192+
193+
Notes:
194+
- SessionReplay depends on Observability. If Observability is missing or listed after SessionReplay, the plugin logs an error and stays inactive.
195+
- Observability runs fine without SessionReplay; adding SessionReplay extends the Observability pipeline to include session recording.
196+
172197
#### Masking sensitive UI
173198

174199
Use `ldMask()` to mark views that should be masked in session replay. There are helpers for both XML-based Views and Jetpack Compose.

sdk/@launchdarkly/observability-android/gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
33

44
[plugins]
5-
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.1.20" }
6-
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.20" }
5+
kotlin-android = { id = "org.jetbrains.kotlin.android", version = "2.2.0" }
6+
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.0" }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ allprojects {
2020
}
2121

2222
dependencies {
23-
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.9.0")
23+
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.10.0")
2424
implementation("com.jakewharton.timber:timber:5.0.1")
2525

2626
// Android

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ 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
65
import com.launchdarkly.sdk.android.LDTimberLogging
76
import io.opentelemetry.api.common.Attributes
87
import kotlin.time.Duration
@@ -29,7 +28,6 @@ const val DEFAULT_BACKEND_URL = "https://pub.observability.app.launchdarkly.com"
2928
* @property disableMetrics Disables metrics if true. Defaults to false.
3029
* @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.
3130
* @property loggerName The name of the logger to use. Defaults to "LaunchDarklyObservabilityPlugin".
32-
* @property instrumentations List of additional instrumentations to use
3331
*/
3432
data class Options(
3533
val serviceName: String = DEFAULT_SERVICE_NAME,
@@ -46,6 +44,4 @@ data class Options(
4644
val disableMetrics: Boolean = false,
4745
val logAdapter: LDLogAdapter = LDTimberLogging.adapter(), // this follows the LaunchDarkly SDK's default log adapter
4846
val loggerName: String = "LaunchDarklyObservabilityPlugin",
49-
// TODO: update this to provide a list of factories instead of instances
50-
val instrumentations: List<LDExtendedInstrumentation> = emptyList()
5147
)

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/InstrumentationManager.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.launchdarkly.observability.client
33
import android.app.Application
44
import com.launchdarkly.logging.LDLogger
55
import com.launchdarkly.observability.api.Options
6+
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
7+
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
68
import com.launchdarkly.observability.interfaces.Metric
79
import com.launchdarkly.observability.network.GraphQLClient
810
import com.launchdarkly.observability.network.SamplingApiService
@@ -12,10 +14,8 @@ import com.launchdarkly.observability.sampling.SamplingConfig
1214
import com.launchdarkly.observability.sampling.SamplingLogProcessor
1315
import com.launchdarkly.observability.sampling.SamplingTraceExporter
1416
import com.launchdarkly.observability.sampling.SpansSampler
15-
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
1617
import io.opentelemetry.android.OpenTelemetryRum
1718
import io.opentelemetry.android.config.OtelRumConfig
18-
import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfig
1919
import io.opentelemetry.android.session.SessionConfig
2020
import io.opentelemetry.api.common.Attributes
2121
import io.opentelemetry.api.logs.Logger
@@ -54,18 +54,25 @@ import java.util.concurrent.TimeUnit
5454
/**
5555
* Manages instrumentation for LaunchDarkly Observability.
5656
*
57+
* This class is responsible for setting up and managing the OpenTelemetry RUM (Real User Monitoring)
58+
* instrumentation. It configures the providers for logs, traces, and metrics based on the
59+
* provided options. It also handles dynamic sampling configuration and provides methods to
60+
* record various telemetry signals like metrics, logs, and traces.
61+
*
5762
* @param application The application instance.
58-
* @param sdkKey The SDK key.
63+
* @param sdkKey The SDK key for authentication.
5964
* @param resources The OpenTelemetry resource describing this service.
60-
* @param logger The logger.
61-
* @param options Additional options.
65+
* @param logger The logger for internal logging.
66+
* @param options Additional configuration options for the SDK.
67+
* @param instrumentations A list of custom instrumentations to be added.
6268
*/
6369
class InstrumentationManager(
6470
private val application: Application,
6571
private val sdkKey: String,
6672
private val resources: Resource,
6773
private val logger: LDLogger,
6874
private val options: Options,
75+
private val instrumentations: List<LDExtendedInstrumentation>,
6976
) {
7077
private val otelRUM: OpenTelemetryRum
7178
private var otelMeter: Meter
@@ -104,7 +111,8 @@ class InstrumentationManager(
104111
resources,
105112
logger,
106113
telemetryInspector,
107-
options
114+
options,
115+
instrumentations
108116
)
109117
logProcessor = processor
110118
sdkLoggerProviderBuilder.addLogRecordProcessor(processor)
@@ -125,7 +133,7 @@ class InstrumentationManager(
125133
}
126134
}
127135

128-
for (instrumentation in options.instrumentations) {
136+
for (instrumentation in instrumentations) {
129137
rumBuilder.addInstrumentation(instrumentation)
130138
}
131139

@@ -402,6 +410,7 @@ class InstrumentationManager(
402410
logger: LDLogger,
403411
telemetryInspector: TelemetryInspector?,
404412
options: Options,
413+
instrumentations: List<LDExtendedInstrumentation>,
405414
): LogRecordProcessor {
406415
val primaryLogExporter = createOtlpLogExporter(options)
407416
sdkLoggerProviderBuilder.setResource(resource)
@@ -432,7 +441,7 @@ class InstrumentationManager(
432441
pipeline to provide instrumentation specific caching and export.
433442
*/
434443
val routingLogRecordProcessor = RoutingLogRecordProcessor(fallthroughProcessor = baseProcessor)
435-
options.instrumentations.forEach { instrumentation ->
444+
instrumentations.forEach { instrumentation ->
436445
instrumentation.getLogRecordProcessor(credential = sdkKey)?.let { processor ->
437446
instrumentation.getLoggerScopeName().let { scopeName ->
438447
routingLogRecordProcessor.addProcessor(scopeName, processor)

sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityClient.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.launchdarkly.observability.client
33
import android.app.Application
44
import com.launchdarkly.logging.LDLogger
55
import com.launchdarkly.observability.api.Options
6+
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
67
import com.launchdarkly.observability.interfaces.Metric
78
import com.launchdarkly.observability.interfaces.Observe
89
import io.opentelemetry.api.common.Attributes
@@ -14,26 +15,34 @@ import io.opentelemetry.sdk.resources.Resource
1415
* The [ObservabilityClient] can be used for recording observability data such as
1516
* metrics, logs, errors, and traces.
1617
*
17-
* It is recommended to use the [Observability] plugin with the LaunchDarkly Android
18-
* Client SDK, as that will automatically initialize the [LDObserve] singleton instance.
18+
* It is recommended to use the [com.launchdarkly.observability.plugin.Observability] plugin with the LaunchDarkly Android
19+
* Client SDK, as that will automatically initialize the [com.launchdarkly.observability.sdk.LDObserve] singleton instance.
1920
*
20-
* @param application The application instance.
21-
* @param sdkKey The SDK key.
22-
* @param resource The resource.
23-
* @param logger The logger.
24-
* @param options Additional options for the client.
2521
*/
2622
class ObservabilityClient : Observe {
2723
private val instrumentationManager: InstrumentationManager
2824

25+
/**
26+
* Creates a new ObservabilityClient.
27+
*
28+
* @param application The application instance.
29+
* @param sdkKey The SDK key for the environment.
30+
* @param resource The resource.
31+
* @param logger The logger.
32+
* @param options Additional options for the client.
33+
* @param instrumentations A list of extended instrumentation providers.
34+
*/
2935
constructor(
3036
application: Application,
3137
sdkKey: String,
3238
resource: Resource,
3339
logger: LDLogger,
34-
options: Options
40+
options: Options,
41+
instrumentations: List<LDExtendedInstrumentation>
3542
) {
36-
this.instrumentationManager = InstrumentationManager(application, sdkKey, resource, logger, options)
43+
this.instrumentationManager = InstrumentationManager(
44+
application, sdkKey, resource, logger, options, instrumentations
45+
)
3746
}
3847

3948
internal constructor(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.launchdarkly.observability.client
2+
3+
import android.app.Application
4+
import com.launchdarkly.logging.LDLogger
5+
import com.launchdarkly.observability.api.Options
6+
7+
/**
8+
* Shared information between plugins.
9+
*/
10+
data class ObservabilityContext(
11+
val sdkKey: String,
12+
val options: Options,
13+
val application: Application,
14+
val logger: LDLogger,
15+
)

0 commit comments

Comments
 (0)