Repository navigation
feat(flutter): Screen views (Navigate) and Rich Clicks - #627
Merged
Merged
Conversation
Flutter renders into a single native Activity/UIViewController, so the native
SDK's automatic screen detection never sees Flutter route changes. Add a
trackScreenView path so navigation shows up on the Session Replay timeline as a
Navigate event (and as a screen_view span):
- Pigeon LDNativeApi.trackScreenView bridged to LDObserve.trackScreenView on
iOS and Android.
- Public LDObserve.trackScreenView(name, {screenClass, screenId, category,
properties}) with a platform-split recorder (native bridge on mobile,
screen_view span gated by analytics.pageViews on web/stub).
- LDNavigatorObserver to auto-emit screen views on route push/pop/replace,
with a customizable screen-name extractor.
- ScreenViewConvention + tests; README docs.
Co-authored-by: Cursor <cursoragent@cursor.com>
Vadman97
approved these changes
Jun 15, 2026
* main: (39 commits) chore: release main (#666) feat: Propagate session replay frameRate, scale, and sampleRate in React Native (#661) ci: skip yarn test for .NET/Python/Ruby plugin-only changes (#664) chore: release main (#663) chore: Skip yarn test for Android and MAUI, Flutter PRs (#662) feat: client-side sampleRate to Android session replay (#660) chore: release main (#659) feat(observability-react-native): expose LDTracer with withSpan via getTracer() (#658) chore: release main (#657) feat(ruby): boot-time OTel auto-instrumentation; keep it working on Rails 7.0 (#643) feat: add GraphQL operation attributes to instrumented spans (#644) chore: release main (#656) feat(observability-react-native): accept plain nested dictionaries in track (#650) chore: release main (#655) fix: backendurl and otlpendpoind pass through (#648) feat(session-replay-react-native): support React Native legacy architecture (RN 0.75+) (#653) fix: MAUI - drop OTel activity instrumentation dependency (#651) chore: release main (#652) feat: supporting advanced trace cases (#645) chore: release main (#649) ... # Conflicts: # sdk/@launchdarkly/flutter/packages/observability/ios/launchdarkly_flutter_observability.podspec # sdk/@launchdarkly/flutter/packages/observability/ios/launchdarkly_flutter_observability/Package.swift
* main: chore: release main (#675) fix(flutter): remove conflicting ios-client-sdk pin in SPM manifest (#674) chore(deps): remediate critical dependency alerts (#673) chore(deps): bump golang.org/x/crypto to v0.52.0 (#672) fix: stabilize flaky Next.js server instrumentation test (#671) chore: release main (#669) feat: add app_reload event with session preservation across reloads (React Native) (#670) fix: use secure RNG for React Native session ids (#668) ci: skip Build Yarn Turborepo job for non-yarn SDK-only changes (#665) # Conflicts: # sdk/@launchdarkly/flutter/packages/observability/ios/launchdarkly_flutter_observability.podspec # sdk/@launchdarkly/flutter/packages/observability/ios/launchdarkly_flutter_observability/Package.swift
* main: (57 commits) chore: release main (#761) fix(highlight.run): never JSON.stringify binary XHR bodies or capture the SDK's own exports (iPad crash root cause) (#760) chore: release main (#759) fix(highlight.run): WebKit/iPad performance hardening — dedupe OpenTelemetry bundles, cap body capture, resync option, telemetry-intake blocklist (#758) chore: release main (#754) fix(highlight.run): record fetch request bodies and headers that are not plain strings or objects (#753) fix(highlight.run): capture XMLHttpRequest bodies and headers on traces without the session replay listener (#752) fix: bump next to 15.5.24 / 16.3.3 for Critical GHSA-2xp9-vwfh-vxw4 (#751) fix(deps): pin image-size to 1.2.1 in the react-native e2e app (#747) chore: release main (#749) feat(go): Add explicit ForceSample marker to the span-kind sampler (#738) chore: release main (#745) fix: bump nanoid, @hono/node-server, and sqlparse to patched versions (#744) fix(highlight-go): allow histogram bucket boundaries, and stop racing on the instrument cache (#740) chore(react-native): add fading overlay examples to session replay masking tab (#742) chore: Update Skip Howdy example (#733) chore: release main (#741) feat(react-native): fix lagging masks and imageQuality option (#734) chore: release main (#736) fix(android): anchor session replay masks to the rendered frame and cover stretch overscroll (Rubber animation) (#739) ...
…let embedders report their own targets Clicks were produced twice, independently: the OTel `click` span was built from automatic tap detection, while Session Replay rebuilt its own `Click` custom event from the same interaction. Manual `LDObserve.trackClick` calls only ever reached the span side, so a click the app reported by hand was missing from the replay timeline. `ObservabilityService` now owns a single `ClickEvent` emitter: automatic taps and `trackClick` both go through `emitClick`, which writes the span and publishes on `ObservabilityContext.clickFlow`, which Session Replay subscribes to. Both click paths now land in both places, in the order they happened. `trackClick` gains `classname`, `xpath` and `timestampMillis`, so a caller that resolves the element itself can describe it as richly as native hit-testing does, and can place the click at the time of the gesture rather than at the time the report arrives across an async bridge. Flutter draws its whole UI into a single `FlutterSurfaceView`, so native hit-testing reports that view for every tap regardless of the widget pressed. The new `LDObserve.setEmbedderClickHandling` lets the Flutter plugin declare that it resolves clicks itself; while set, `UserInteractionManager` skips taps landing on an embedder surface so the tap is reported once, by the side that can name the widget. The suppression is scoped to that surface, so an add-to-app host's own native screens keep reporting their real targets, and it is off until Dart actually installs its detection, so an app without the plugin keeps the coarse native clicks instead of silently reporting none. Co-authored-by: Cursor <cursoragent@cursor.com>
…efore init The embedder's plugin installs its click detection when its own widget tree mounts, which regularly happens before observability is initialized. The setter only wrote through to a live client, so an early call vanished: native detection kept describing the Flutter surface while Dart also reported the tap, duplicating every click. The requested state now lives on the companion and is applied when a client is installed, so the handshake no longer depends on the two sides booting in a particular order. `embedderHandlesClicks` becomes `@Volatile` because the embedder sets it from whichever thread its plugin runs on while touch dispatch reads it on the main thread. Co-authored-by: Cursor <cursoragent@cursor.com>
Reading the retained request and copying it onto the manager were two steps, so a request arriving between them was applied and then overwritten by the older value - leaving the handshake stuck wrong for the rest of the session. Both paths now hold one lock, and the client is still published before the apply so a later request finds a manager to write through to. Co-authored-by: Cursor <cursoragent@cursor.com>
…er-sr-screenview Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ 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 23e2ad0. Configure here.
# Conflicts: # sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/client/ObservabilityService.kt # sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/interfaces/Observe.kt # sdk/@launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/sdk/LDObserve.kt # sdk/@launchdarkly/observability-android/lib/src/test/kotlin/com/launchdarkly/observability/sdk/LDObserveTest.kt
abelonogov-ld
enabled auto-merge (squash)
September 15, 2026 21:27
Merged
abelonogov-ld
pushed a commit
that referenced
this pull request
Sep 15, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>launchdarkly-observability-android: 0.68.0</summary> ## [0.68.0](launchdarkly-observability-android-0.67.0...launchdarkly-observability-android-0.68.0) (2026-09-15) ### Features * **flutter:** record screen views (Navigate) from Dart ([#627](#627)) ([cdd2190](cdd2190)) </details> <details><summary>launchdarkly_flutter_observability: 0.17.0</summary> ## [0.17.0](launchdarkly_flutter_observability-0.16.0...launchdarkly_flutter_observability-0.17.0) (2026-09-15) ### Features * **flutter:** record screen views (Navigate) from Dart ([#627](#627)) ([cdd2190](cdd2190)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Overview** > **Release Please** bumps **`launchdarkly_flutter_observability`** to **0.17.0** and **`launchdarkly-observability-android`** to **0.68.0**, and updates `.release-please-manifest.json`, package versions (`pubspec.yaml`, `_packageVersion` in the native bridge, `gradle.properties`), and both **CHANGELOG** files. > > The new changelog entries document **0.17.0 / 0.68.0** as shipping **Flutter screen-view recording from Dart** (Session Replay **Navigate** events, [#627](#627)). This PR does not include that implementation—only the versioned release artifacts for publishing. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 71f4426. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
3 tasks
abelonogov-ld
added a commit
that referenced
this pull request
Sep 15, 2026
## Summary - Pin `launchdarkly_flutter_observability` to published `launchdarkly-observability-android` **0.68.0** (from 0.66.1). - 0.68.0 is the Android artifact that includes the click funnel from #763 plus the `trackClick` parameter order shipped with Flutter screen views / Dart clicks (#627). The plugin already calls that API with named arguments. ## Test plan - [ ] Confirm 0.68.0 is on Maven Central before merging. - [ ] Flutter Observability CI (analyze / unit tests) is green. - [ ] Optional: `flutter run` the example on an Android emulator and tap a button / change a named route; click and Navigate still reach native. Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Phase 1 of adding Flutter-sourced Session Replay events (see plan). Flutter renders into a single native
Activity/UIViewController, so the native SDK's automatic screen detection never sees Flutter route changes. This adds atrackScreenViewpath so navigation appears on the Session Replay timeline as aNavigateevent (and as ascreen_viewspan).LDNativeApi.trackScreenView(name, screenClass, screenId, category, properties), wired on iOS/Android to the existing publicLDObserve.trackScreenView(which funnels into thescreen_viewspan + Session ReplayNavigate).LDObserve.trackScreenView(name, {screenClass, screenId, category, properties}), with a platform-splitScreenViewRecorder(native bridge on mobile; ascreen_viewspan gated byanalytics.pageViewson web/stub).LDNavigatorObserver(aNavigatorObserver) emits screen views ondidPush/didPop/didReplaceusingroute.settings.name, with a customizablescreenNameExtractorand optionalcategory.ScreenViewConvention(span namescreen_view,event.name/event.screen_class/event.screen_id/event.category); newpageViewsEnabledconfig mapped fromanalytics.pageViews.Why
AnalyticsOptions.pageViewsis native-only (Android lifecycle / iOS no-op) and cannot observe Flutter'sNavigator, so screen views had no way to reach Session Replay from a Flutter app.Test plan
dart analyzeclean,dart formatcleanflutter test(153 tests) pass, incl. newscreen_view_convention_test.dartLDObserve.trackScreenView('Home')andLDNavigatorObserverproduceNavigateevents in a Session Replay recording (iOS + Android)screen_viewspan emitted whenanalytics.pageViewsis enabled, suppressed when disabledFollow-up
Phase 2 (proper RRWeb
Clickwith Flutter widget info) is tracked separately and requires native additions across all three repos.Made with Cursor