Skip to content

feat(snippets): port Android observability plugin docs to canonical snippets#536

Draft
kinyoklion wants to merge 7 commits into
mainfrom
rlamb/sdk-2642/obs-android-feature-docs
Draft

feat(snippets): port Android observability plugin docs to canonical snippets#536
kinyoklion wants to merge 7 commits into
mainfrom
rlamb/sdk-2642/obs-android-feature-docs

Conversation

@kinyoklion

Copy link
Copy Markdown
Member

Ports the Android observability plugin reference page (fern/topics/sdk/observability/android.mdx, 29 code blocks) to canonical snippets — the first tranche of the observability section. Type-checked against the real published plugin launchdarkly-observability-android:0.60.0 (the docs' install block pinned a stale 0.21.0).

Validation (real type-check)

  • Kotlin + Java blocks (21) → the existing android-client validator (compileDebug{Kotlin,JavaWithJavac}), with the observability plugin bumped to 0.60.0 and opentelemetry-api:1.51.0 + okhttp added. New kotlin-observability, kotlin-observability-coroutine, and java-observability scaffolds. OTel types are imported explicitly (not wildcard): the plugin aar ships a com.launchdarkly.observability.replay.Attributes that otherwise shadows io.opentelemetry.api.common.Attributes for the unqualified name the fragments use.
  • Compose / View masking blocks (3) → a new dedicated android-compose validator (Kotlin 2.0.21 + Compose 1.7.x). The plugin's Compose masking API (Modifier.ldMask()) is built against Compose 1.7.x, which carries Kotlin-2.0 metadata the Kotlin-1.8 android-client validator can't read — so it gets its own project rather than risking a Kotlin bump of the shared validator. The View block's R.layout.activity_login/R.id.password resolve against a layout baked into that validator.
  • 5 rendered-only: 2 Gradle dependency install blocks (Android AAR coordinates can't resolve in shell-install's plain-JVM gradle project — the android-client validator compiles against the exact artifacts), 2 byte-buddy instrumentation Gradle config blocks, and the W3C trace-context header example.

Snippet fixes (reconciled to 0.60.0 / valid code)

  • Instrumentations(activityLifecycle=…)userTaps/screens (param removed in 0.60.0).
  • ReplayOptions(capturePeriodMillis=…) → removed (no such param).
  • Truncated tracing fragment span.enspan.end().
  • Java Collections.singletonList<Plugin>(Observability(...)) (Kotlin syntax) → Collections.<Plugin>singletonList(new Observability(...)).

Plugin Java-interop gaps found (SDK follow-up)

The Java examples only compile in non-idiomatic forms because the plugin lacks Java-friendly annotations:

  • LDObserve is a companion object with no @JvmStatic → Java calls LDObserve.Companion.startSpan(...) (matches observability-sdk#605's own pure-Java e2e).
  • The Observability constructor has no @JvmOverloads (and ObservabilityOptions is a Kotlin data class with no Java no-arg constructor) → Java init must use the full 4-arg new Observability(app, key, ObservabilityOptions.builder().build(), null). Adding @JvmOverloads would let the docs use the clean 2-arg form.

Phase B (ld-docs markers) follows once this merges + a snippets release is cut. Part of the docs-porting effort (SDK-2434).

…real compile

Port the Android observability plugin page's Kotlin blocks (15: install/
initialize, plugin options, session replay config + start/stop/flush,
privacy profiles, distributed tracing) to canonical snippets,
type-checked against the real com.launchdarkly:launchdarkly-observability-android
0.60.0 aar via compileDebugKotlin in the android-client validator.

- Bump the baked observability plugin to 0.60.0; add opentelemetry-api
  1.51.0 + okhttp to the validator so tracing fragments type-check.
- New kotlin-observability scaffold. Imports the OTel types explicitly
  rather than by wildcard: the observability aar ships a
  com.launchdarkly.observability.replay.Attributes that would otherwise
  shadow io.opentelemetry.api.common.Attributes for the unqualified name
  the fragments use.
- Reconcile two blocks to the current 0.60.0 API: Instrumentations
  dropped activityLifecycle (now userTaps/screens); ReplayOptions
  dropped capturePeriodMillis.

Part of the observability section port (android tranche).
Java blocks (init, configure options, 3 distributed-tracing) type-checked
against the real plugin via a new java-observability scaffold. Reconciled
to the plugin's actual Java interop (confirmed against observability-sdk
#605's pure-Java e2e):
- LDObserve is a companion object with no @JvmStatic, so Java calls it as
  LDObserve.Companion.startSpan(...).
- The Observability plugin constructor has no @jvmoverloads, so Java passes
  the full 4-arg form with ObservabilityOptions.builder().build() (the
  data class has no Java no-arg constructor) and a trailing null.

Gradle install blocks are rendered from canonical but not validated:
Android AAR coordinates can't be resolved by the shell-install plain-JVM
gradle project.

Part of the observability section port (android tranche).
…ated build/http blocks

- Fix the truncated distributed-tracing 'log with context' fragment
  (span.en -> span.end()) and add a coroutine-scope scaffold variant so
  its launch(Dispatchers.IO) resolves a CoroutineScope receiver.
- Render (unvalidated) the byte-buddy instrumentation gradle config blocks
  and the W3C trace-context header example (build config / headers, not
  compilable code).

Part of the observability section port (android tranche).
…mpose validator

New dedicated android-compose validator (Kotlin 2.0.21 + Compose 1.7.x) that
type-checks the plugin's Compose Modifier.ldMask()/ldUnmask() and the View
EditText.ldMask() masking fragments against the real plugin. Separate from the
Kotlin-1.8 android-client validator because the plugin's Compose masking API
carries Kotlin-2.0 metadata that the older compiler can't read.

Completes the android observability tranche (29/29 blocks): 24 real type-checked
across the two validators, 5 rendered-only (gradle install/config + http header).
Adds android-observability-port-notes.md documenting fixes + the plugin's Java
interop gaps.

Part of the observability section port (android tranche).
Validates the observability group across both the android-client and
android-compose validators (one group row drives both).
@kinyoklion kinyoklion changed the title Port Android observability plugin docs to canonical snippets feat(snippets): port Android observability plugin docs to canonical snippets Jul 7, 2026
…ity constructor

Bumping the shared android-client validator's observability plugin from
0.54.0 to 0.60.0 (for the observability port) added a 4th customSessionId
param to the Observability constructor. With no @jvmoverloads, the existing
initialize-the-client-android-sdk-v5-x-java example's 3-arg call no longer
compiles from Java; add the trailing null to match the current 4-arg
constructor. The Kotlin sibling is unaffected (default args).

```java
implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+'
implementation 'com.launchdarkly:launchdarkly-observability-android:0.60.0'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version updated to the current plugin (0.60.0). Original doc snippet:

implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+'
implementation 'com.launchdarkly:launchdarkly-observability-android:0.21.0'


```kotlin
implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.+")
implementation("com.launchdarkly:launchdarkly-observability-android:0.60.0")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version updated to the current plugin (0.60.0). Original doc snippet:

implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.+")
implementation("com.launchdarkly:launchdarkly-observability-android:0.21.0")

instrumentations = ObservabilityOptions.Instrumentations(
crashReporting = false,
launchTime = true,
userTaps = true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconciled to the 0.60.0 API: Instrumentations dropped activityLifecycle (now userTaps/screens). Original doc snippet:

val mobileKey = "example-mobile-key"

val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(
            this@BaseApplication,
            mobileKey,
            ObservabilityOptions(
              serviceName = "my-android-app",
              serviceVersion = "1.0.0",
              debug = true,
              logsApiLevel = ObservabilityOptions.LogLevel.WARN,
              tracesApi = ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = false),
              metricsApi = ObservabilityOptions.MetricsApi.disabled(),
              instrumentations = ObservabilityOptions.Instrumentations(
                crashReporting = false,
                activityLifecycle = true,
                launchTime = true
              ),
              resourceAttributes = Attributes.of(
                AttributeKey.stringKey("environment"), "production",
                AttributeKey.stringKey("team"), "mobile"
              ),
              customHeaders = mapOf(
                "X-Custom-Header" to "custom-value"
              )
            )
          )
        )
      )
    )
    .build()

maskTextInputs = true,
maskText = true
),
debug = false

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reconciled to 0.60.0: ReplayOptions.capturePeriodMillis was removed. Original doc snippet:

import com.launchdarkly.observability.plugin.Observability
import com.launchdarkly.observability.replay.plugin.SessionReplay
import com.launchdarkly.observability.replay.ReplayOptions
import com.launchdarkly.observability.replay.PrivacyProfile

val mobileKey = "example-mobile-key"

val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(this@BaseApplication, mobileKey),
          SessionReplay(
            options = ReplayOptions(
              privacyProfile = PrivacyProfile(
                maskTextInputs = true,
                maskText = true
              ),
              capturePeriodMillis = 1000,
              debug = false
            )
          )
        )
      )
    )
    .build()

)
// Capture span context while still on the originating thread.
val capturedContext = span.makeCurrent().use { span.spanContext }
span.end()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original was truncated at span.en; completed to span.end(). Original doc snippet:

import io.opentelemetry.context.Context

val parentSpan = LDObserve.startSpan("parentSpan", Attributes.empty())

// Capture the current context, which includes the active span
val context = Context.current()

launch(Dispatchers.IO) {
    val span = LDObserve.startSpan(
        name = "log-context-demo",
        attributes = Attributes.of(
            AttributeKey.stringKey("demo"), "log-with-context"
        )
    )
    // Capture span context while still on the originating thread.
    val capturedContext = span.makeCurrent().use { span.spanContext }
    span.en
    // Simulate a detached thread where OTel context is lost automatically.
    // Span.current() here returns INVALID, so we pass the captured context explicitly.
    Thread {
        Span.wrap(capturedContext).makeCurrent().use {
            val childSpan = LDObserve.startSpan("child of log-context-demo", Attributes.empty())
            childSpan.end()
        }
        LDObserve.recordLog(
            message = text,
            severity = Severity.WARN,
            attributes = Attributes.of(
                AttributeKey.stringKey("source"), "detached-thread-demo"
            ),
            spanContext = capturedContext
        )
    }.start()
}

parentSpan.end()

LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
.mobileKey(mobileKey)
.plugins(Components.plugins().setPlugins(
Collections.<Plugin>singletonList(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten to valid Java for the 0.60.0 constructor (Collections.<Plugin>singletonList + new + the 4-arg form). Original doc snippet:

String mobileKey = "example-mobile-key";

LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(Components.plugins().setPlugins(
      Collections.singletonList<Plugin>(Observability(this.getApplication(), mobileKey))
    ))
    // other options
    .build();

// You'll need this context later, but you can ignore it for now.
LDContext context = LDContext.create("example-context-key");

LDClient client = LDClient.init(this.getApplication(), ldConfig, context, 0);

new Observability(
this.getApplication(),
mobileKey,
ObservabilityOptions.builder()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten to valid Java (no Kotlin named/default args): builder + 4-arg constructor. Original doc snippet:

String mobileKey = "example-mobile-key";

LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        Collections.singletonList<Plugin>(
          Observability(
            this.getApplication(),
            mobileKey,
            ObservabilityOptions(
              resourceAttributes = Attributes.of(
                AttributeKey.stringKey("serviceName"), "example-service"
              )
            )
          )
        )
      )
    )
    .build();

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;

Span parentSpan = LDObserve.Companion.startSpan("parentSpan", Attributes.empty());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LDObserve is a companion object with no @JvmStatic, so Java calls LDObserve.Companion.startSpan(...). Original doc snippet:

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;

Span parentSpan = LDObserve.startSpan("parentSpan", Attributes.empty());
try (Scope parentScope = parentSpan.makeCurrent()) {
    Context context = Context.current();

    new Thread(() -> {
        try (Scope childScope = context.makeCurrent()) {
            Span childSpan = LDObserve.startSpan("childSpan", Attributes.empty());
            // do work
            childSpan.end();
        }
    }).start();
} finally {
    parentSpan.end();
}

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;

Span parentSpan = LDObserve.Companion.startSpan("parentSpan", Attributes.empty());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LDObserve.Companion for Java, and removed a stray extra closing brace. Original doc snippet:

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;

Span parentSpan = LDObserve.startSpan("parentSpan", Attributes.empty());
try (Scope parentScope = parentSpan.makeCurrent()) {
    // Now parentSpan is active in Context.current()
    Context ctx = Context.current();

    // Later, in another thread, restore the context
    executor.execute(() -> {
        try (Scope scope = ctx.makeCurrent()) {
            Span nestedSpan = LDObserve.startSpan("nestedSpan", Attributes.empty());
            // do work — nestedSpan is a child of parentSpan
            nestedSpan.end();
        }
    });
} finally {
    parentSpan.end();
}}

.plugins(Components.plugins().setPlugins(
Collections.<Plugin>singletonList(
new Observability(this.getApplication(), "example-mobile-key", ObservabilityOptions.builder().build())
new Observability(this.getApplication(), "example-mobile-key", ObservabilityOptions.builder().build(), null)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 0.60.0 Observability constructor added a 4th customSessionId param (no @JvmOverloads), so the 3-arg call needed a trailing null. Original snippet:

LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey("example-mobile-key")
    // optional observability plugin, requires LaunchDarkly Android Client SDK v5.9+
    .plugins(Components.plugins().setPlugins(
      Collections.<Plugin>singletonList(
        new Observability(this.getApplication(), "example-mobile-key", ObservabilityOptions.builder().build())
      )
    ))
    // other options
    .build();

// You'll need this context later, but you can ignore it for now.
LDContext context = LDContext.create("example-context-key");

LDClient client = LDClient.init(this.getApplication(), ldConfig, context, 0);

Reconcile the android observability snippets with the current android.mdx,
which grew a product-analytics section and a manual-instrumentation section
and moved the distributed-tracing blocks to the plugin's map-based API.

- Update tracing snippets to the map-based style (startSpan(name,
  properties=...), 1-arg startSpan). Java keeps the compilable
  LDObserve.Companion form since the companion has no @JvmStatic.
- Add snippets: import-java/kotlin, configure-product-analytics-kotlin,
  record-logs[-typed]-kotlin, record-traces[-typed]-kotlin,
  track-event-kotlin, track-screen-view-kotlin. All type-check against
  observability-android 0.60.0.
- Analytics omits pageViews (not a field on 0.60.0 or plugin main).
- Scaffolds gain a performDatabaseQuery() stub and java.util Map imports.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant