Skip to content

Commit 23047d7

Browse files
committed
Add Session Replay plugin
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.
1 parent 50723a3 commit 23047d7

File tree

12 files changed

+236
-59
lines changed

12 files changed

+236
-59
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.9.2")
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: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.launchdarkly.observability.plugin.Observability
1212
import com.launchdarkly.observability.replay.PrivacyProfile
1313
import com.launchdarkly.observability.replay.ReplayInstrumentation
1414
import com.launchdarkly.observability.replay.ReplayOptions
15+
import com.launchdarkly.observability.replay.SessionReplay
1516
import com.launchdarkly.sdk.android.LDAndroidLogging
1617
import com.launchdarkly.sdk.android.integrations.Plugin
1718
import io.opentelemetry.api.common.AttributeKey
@@ -32,14 +33,6 @@ open class BaseApplication : Application() {
3233
),
3334
debug = true,
3435
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-
),
4336
)
4437

4538
var telemetryInspector: TelemetryInspector? = null
@@ -52,6 +45,12 @@ open class BaseApplication : Application() {
5245
options = testUrl?.let { pluginOptions.copy(backendUrl = it, otlpEndpoint = it) } ?: pluginOptions
5346
)
5447

48+
val replayPlugin = SessionReplay(
49+
options = ReplayOptions(
50+
privacyProfile = PrivacyProfile(maskText = false)
51+
)
52+
)
53+
5554
// Set LAUNCHDARKLY_MOBILE_KEY to your LaunchDarkly mobile key found on the LaunchDarkly
5655
// dashboard in the start guide.
5756
// If you want to disable the Auto EnvironmentAttributes functionality.
@@ -60,7 +59,10 @@ open class BaseApplication : Application() {
6059
.mobileKey(LAUNCHDARKLY_MOBILE_KEY)
6160
.plugins(
6261
Components.plugins().setPlugins(
63-
Collections.singletonList<Plugin>(observabilityPlugin)
62+
listOf(
63+
observabilityPlugin,
64+
replayPlugin
65+
)
6466
)
6567
)
6668
.build()

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/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.9.2")
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: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.launchdarkly.observability.sampling.SamplingLogProcessor
1313
import com.launchdarkly.observability.sampling.SamplingTraceExporter
1414
import com.launchdarkly.observability.sampling.SpansSampler
1515
import com.launchdarkly.observability.coroutines.DispatcherProviderHolder
16+
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
1617
import io.opentelemetry.android.OpenTelemetryRum
1718
import io.opentelemetry.android.config.OtelRumConfig
1819
import io.opentelemetry.android.features.diskbuffering.DiskBufferingConfig
@@ -54,18 +55,25 @@ import java.util.concurrent.TimeUnit
5455
/**
5556
* Manages instrumentation for LaunchDarkly Observability.
5657
*
58+
* This class is responsible for setting up and managing the OpenTelemetry RUM (Real User Monitoring)
59+
* instrumentation. It configures the providers for logs, traces, and metrics based on the
60+
* provided options. It also handles dynamic sampling configuration and provides methods to
61+
* record various telemetry signals like metrics, logs, and traces.
62+
*
5763
* @param application The application instance.
58-
* @param sdkKey The SDK key.
64+
* @param sdkKey The SDK key for authentication.
5965
* @param resources The OpenTelemetry resource describing this service.
60-
* @param logger The logger.
61-
* @param options Additional options.
66+
* @param logger The logger for internal logging.
67+
* @param options Additional configuration options for the SDK.
68+
* @param instrumentations A list of custom instrumentations to be added.
6269
*/
6370
class InstrumentationManager(
6471
private val application: Application,
6572
private val sdkKey: String,
6673
private val resources: Resource,
6774
private val logger: LDLogger,
6875
private val options: Options,
76+
private val instrumentations: List<LDExtendedInstrumentation>,
6977
) {
7078
private val otelRUM: OpenTelemetryRum
7179
private var otelMeter: Meter
@@ -104,7 +112,8 @@ class InstrumentationManager(
104112
resources,
105113
logger,
106114
telemetryInspector,
107-
options
115+
options,
116+
instrumentations
108117
)
109118
logProcessor = processor
110119
sdkLoggerProviderBuilder.addLogRecordProcessor(processor)
@@ -125,7 +134,7 @@ class InstrumentationManager(
125134
}
126135
}
127136

128-
for (instrumentation in options.instrumentations) {
137+
for (instrumentation in instrumentations) {
129138
rumBuilder.addInstrumentation(instrumentation)
130139
}
131140

@@ -412,6 +421,7 @@ class InstrumentationManager(
412421
logger: LDLogger,
413422
telemetryInspector: TelemetryInspector?,
414423
options: Options,
424+
instrumentations: List<LDExtendedInstrumentation>,
415425
): LogRecordProcessor {
416426
val primaryLogExporter = createOtlpLogExporter(options)
417427
sdkLoggerProviderBuilder.setResource(resource)
@@ -442,7 +452,7 @@ class InstrumentationManager(
442452
pipeline to provide instrumentation specific caching and export.
443453
*/
444454
val routingLogRecordProcessor = RoutingLogRecordProcessor(fallthroughProcessor = baseProcessor)
445-
options.instrumentations.forEach { instrumentation ->
455+
instrumentations.forEach { instrumentation ->
446456
instrumentation.getLogRecordProcessor(credential = sdkKey)?.let { processor ->
447457
instrumentation.getLoggerScopeName().let { scopeName ->
448458
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(
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.launchdarkly.observability.client
2+
3+
import com.launchdarkly.observability.plugin.InstrumentationContributor
4+
import com.launchdarkly.observability.plugin.Observability
5+
import com.launchdarkly.sdk.android.LDClient
6+
import com.launchdarkly.sdk.android.integrations.Plugin
7+
import java.util.WeakHashMap
8+
9+
/**
10+
* Manages a collection of plugins associated with an [LDClient] instance.
11+
*
12+
* This object provides a central place to register and retrieve plugins for a given LDClient.
13+
* It uses a [WeakHashMap] to store plugins, which allows the [LDClient] instances and
14+
* associated plugins to be garbage collected when they are no longer in use.
15+
*/
16+
internal object PluginManager {
17+
private val plugins = WeakHashMap<LDClient, MutableList<Plugin>>()
18+
19+
/**
20+
* Adds a [Plugin] to the list of plugins associated with the given [LDClient].
21+
*
22+
* If no plugins have been added for the client before, a new list is created.
23+
*
24+
* @param client The [LDClient] to associate the plugin with.
25+
* @param plugin The [Plugin] to add.
26+
*/
27+
fun add(client: LDClient, plugin: Plugin) {
28+
synchronized(plugins) {
29+
plugins.getOrPut(client) { mutableListOf() }.add(plugin)
30+
}
31+
}
32+
33+
/**
34+
* Retrieves the list of [Plugin]s associated with the given [LDClient].
35+
*
36+
* The returned list is a snapshot and is safe to iterate over.
37+
*
38+
* @param client The [LDClient] to get the plugins for.
39+
* @return A list of [Plugin]s, or null if no plugins are associated with the client.
40+
*/
41+
fun get(client: LDClient): List<Plugin>? = synchronized(plugins) {
42+
plugins[client]?.toList()
43+
}
44+
45+
/**
46+
* Retrieves the list of [InstrumentationContributor] plugins associated with the given [LDClient].
47+
*
48+
* @param client The [LDClient] to get the instrumentations for.
49+
* @return A list of [InstrumentationContributor]s, or null if no plugins are associated with the client.
50+
*/
51+
fun getInstrumentations(client: LDClient): List<InstrumentationContributor>? = synchronized(plugins) {
52+
plugins[client]?.filterIsInstance<InstrumentationContributor>()
53+
}
54+
55+
/**
56+
* Checks if the observability plugin has been initialized for the given [LDClient].
57+
*
58+
* @param client The [LDClient] to check.
59+
* @return True if the observability plugin is initialized, false otherwise.
60+
*/
61+
fun isObservabilityInitialized(client: LDClient): Boolean {
62+
return get(client)?.any { it.metadata.name == Observability.PLUGIN_NAME } == true
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.launchdarkly.observability.plugin
2+
import com.launchdarkly.observability.interfaces.LDExtendedInstrumentation
3+
4+
/**
5+
* Plugins can implement this to contribute OpenTelemetry instrumentations that should
6+
* be installed by the Observability plugin.
7+
*/
8+
interface InstrumentationContributor {
9+
fun provideInstrumentations(): List<LDExtendedInstrumentation>
10+
}

0 commit comments

Comments
 (0)