Skip to content

feat(observability-android): init observability with or without the LaunchDarkly client- #743 - #750

Draft
abelonogov-ld wants to merge 8 commits into
mainfrom
andrey/independent-launch2
Draft

abelonogov-ld wants to merge 8 commits into
mainfrom
andrey/independent-launch2

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Pushed for feedback on the API shape — see the note on dependencies below before running CI. The iOS counterpart is launchdarkly/swift-launchdarkly-observability#272.

Why

LDObserve.init took a mobile key only, so instrumenting an app that uses feature flags meant handing plugins to LDConfig and configuring telemetry through the flagging SDK's configuration.

What

LDObserve.init now also accepts an already initialized LDClient, and both paths take the same arguments:

// no feature-flag SDK in the app
LDObserve.init(application, LAUNCHDARKLY_MOBILE_KEY, ldContext, observability, replay)

// instrumenting an initialized client
LDObserve.init(application, ldClient, ldContext, observability, replay)

The client path registers the plugins through LDClient.registerPlugin, so the client's own configuration is left untouched, and Observability is registered before Session Replay because replay reads the ObservabilityContext that Observability publishes.

Identity now runs through one funnel. It previously arrived only through the client's afterIdentify hook, which left the standalone path unable to attribute telemetry (replays were recorded as unknown) and missed the identify LDClient performs at startup. Every identify now goes through ObservabilityService: it caches the context keys stamped onto later spans, emits the LD.identify log, and broadcasts an IdentifyEvent. Consequences:

  • New LDObserve.identify(key, attributes) and identify(contextKeys, canonicalKey, attributes) feed that same funnel, mirroring track.
  • SessionReplayHook is deleted: replay collects the broadcast, and applies identifies idempotently (same attributes and session_id) so a cross-platform bridge cannot double-record one.
  • LDObserve.isFlagClientInitialized reports whether observability is attached to a client, for hosts that support both setups.

The e2e app demonstrates both paths (initWithFlagClient() / initIndependently(), selected by isIndependent) in the Compose and Java flavors, hides the LDClient-driven controls when no client is initialized, and shows identify through both LDClient and LDObserve — including an anonymous identity that imitates what generateAnonymousKeys produces.

Dependencies — CI will not build this yet

LDClient.registerPlugin is not in a released Android SDK. Locally this was built against a checkout of android-client-sdk carrying that API, and the local dependency wiring is deliberately left out of this PR: no mavenLocal(), no composite build, and the client SDK pin is untouched at 5.14.0, which has no registerPlugin. Compilation fails until the client SDK ships it and the pin is bumped.

Test plan

  • :lib:testDebugUnitTest passes, including the new IdentifyFunnelTest and LDObserveClientInitTest
  • e2e app with isIndependent = false: flag evaluations, LDClient.identify and LDClient.track are instrumented and appear on the replay
  • e2e app with isIndependent = true: telemetry and the replay session are attributed to LDObserve.identify, LDClient controls are hidden
  • Robolectric e2e suite (TestApplication.initForTest) still green

Note

Overview
LDObserve.init can attach to an already-initialized LDClient (same options as standalone mobile-key init) via registerPlugin, deprecating wiring Observability / SessionReplay through LDConfig.plugins. Session replay installs directly off the published ObservabilityContext instead of as a second client plugin with its own hook.

Identity is unified in one funnel: new LDObserve.identify(...) APIs, an identifyFlow / IdentifyEvent broadcast, and recordIdentify replace hook-only caching. SessionReplayHook is removed; replay subscribes to the funnel and dedupes redundant identifies. LDObserve.isFlagClientInitialized lets hosts hide LDClient-only UI. Init seeds the starting LDObserveContext through the same path.

Behavioral / reliability changes: automatic crash reporting defaults to off (opt-in via instrumentations.crashReporting). Late init adopts the on-screen window/activity for touches and screen views; touch capture is marshaled on the main thread. Touch hook attachment happens earlier in both init paths.

E2E sample app switches to initWithFlagClient() / initIndependently() (isIndependent), documents LDObserve identify (including persisted anonymous keys), and gates flag/track/identify LDClient controls. Android client SDK pin → 5.15.0; local mavenLocal / composite android-client-sdk is added for dev builds.

Reviewed by Cursor Bugbot for commit 9c73b47. Bugbot is set up for automated code reviews on this repo. Configure here.

abelonogov-ld and others added 6 commits August 19, 2026 19:28
…aunchDarkly client

LDObserve.init took a mobile key only, so instrumenting an app that uses feature
flags meant passing plugins to LDConfig, configuring telemetry through the
flagging SDK's configuration.

It now also accepts an already initialized LDClient, registering the plugins on
it so the client's own configuration is left untouched, and both paths take the
same arguments.

Identity previously arrived only through the client's afterIdentify hook, which
left the standalone path with no way to attribute telemetry and missed the
identify LDClient performs at startup. Every identify now runs through one
funnel in ObservabilityService: it caches the context keys, emits the
LD.identify log and broadcasts an IdentifyEvent. The new LDObserve.identify
overloads feed the same funnel, and SessionReplayHook is gone because replay
collects the broadcast instead of hooking the client.

Co-authored-by: Cursor <cursoragent@cursor.com>
…it runs late

Touch capture and screen detection learned about activities only through
ActivityLifecycleCallbacks, which are never replayed. Initializing from a
background thread loses the race with the activity launch, so the launch
screen produced no click spans, no screen_view, and an "unknown" screen on
the initial identify until the app was backgrounded and brought back.

Initialization now attaches the touch hook first thing, and where that still
loses, ObservabilityService adopts the window already on screen. It resolves
the window through WindowInspector rather than the decor view's context,
which is a DecorContext over the application context and so never unwraps to
an activity. Taps only need the window; the activity is resolved separately
for the screen report.

Window state is also confined to the main thread, since Session Replay
enables capture from its own coroutine and Window.callback is a plain field
whose cross-thread write may never become visible to the main thread.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner August 29, 2026 01:05
@abelonogov-ld
abelonogov-ld marked this pull request as draft August 29, 2026 01:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3b76ed7. Configure here.

CoroutineScope(Dispatchers.Default + SupervisorJob()).launch {
replayService.identifySession(ldContext)
identify(contextKeys, ldContext.fullyQualifiedKey, attributes = null)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seed identify can miss replay

High Severity

The initial identify is emitted on a SharedFlow with replay = 0 from a later Dispatchers.Default coroutine. extraBufferCapacity does not keep values for future subscribers, so if Session Replay's collector has not entered collect yet the seed is dropped. Standalone replays then stay attributed as unknown, which is the gap this change set out to close. The old path called identifySession on the live service directly and did not have this race.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b76ed7. Configure here.

CoroutineScope(Dispatchers.Default + SupervisorJob()).launch {
replayService.identifySession(ldContext)
identify(contextKeys, ldContext.fullyQualifiedKey, attributes = null)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seed drops context attributes

Medium Severity

seedInitialIdentify always passes attributes = null, so name and anonymous from the init LDObserveContext never reach the identify funnel. The LD.identify log and the replay identify omit those fields, and standalone sessions started with an anonymous or named context are not recorded that way.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b76ed7. Configure here.

substitute(module("com.launchdarkly:launchdarkly-android-client-sdk"))
.using(project(":launchdarkly-android-client-sdk"))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Local SDK wiring breaks builds

High Severity

An unconditional includeBuild of ../../../../android-client-sdk (outside this repo), plus mavenLocal() and an AGP pin to 8.3.2, was committed. Gradle fails settings evaluation when that checkout is missing, so CI and any clean clone cannot configure the project. The PR text said this local wiring would stay out.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3b76ed7. Configure here.

This branch has not been deployed

No deployments
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