Skip to content

feat(flutter): Screen views (Navigate) and Rich Clicks - #627

Merged
abelonogov-ld merged 21 commits into
mainfrom
andrey/flutter-sr-screenview
Sep 15, 2026
Merged

abelonogov-ld merged 21 commits into
mainfrom
andrey/flutter-sr-screenview

Conversation

@abelonogov-ld

Copy link
Copy Markdown
Contributor

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 a trackScreenView path so navigation appears on the Session Replay timeline as a Navigate event (and as a screen_view span).

  • Bridge: new Pigeon LDNativeApi.trackScreenView(name, screenClass, screenId, category, properties), wired on iOS/Android to the existing public LDObserve.trackScreenView (which funnels into the screen_view span + Session Replay Navigate).
  • Public API: LDObserve.trackScreenView(name, {screenClass, screenId, category, properties}), with a platform-split ScreenViewRecorder (native bridge on mobile; a screen_view span gated by analytics.pageViews on web/stub).
  • Auto-capture: LDNavigatorObserver (a NavigatorObserver) emits screen views on didPush/didPop/didReplace using route.settings.name, with a customizable screenNameExtractor and optional category.
  • Conventions/config: ScreenViewConvention (span name screen_view, event.name/event.screen_class/event.screen_id/event.category); new pageViewsEnabled config mapped from analytics.pageViews.
  • Docs + unit tests. All additive (no deletions); Pigeon regen only adds the new method.

Why

AnalyticsOptions.pageViews is native-only (Android lifecycle / iOS no-op) and cannot observe Flutter's Navigator, so screen views had no way to reach Session Replay from a Flutter app.

Test plan

  • dart analyze clean, dart format clean
  • flutter test (153 tests) pass, incl. new screen_view_convention_test.dart
  • On-device: LDObserve.trackScreenView('Home') and LDNavigatorObserver produce Navigate events in a Session Replay recording (iOS + Android)
  • Web: screen_view span emitted when analytics.pageViews is enabled, suppressed when disabled

Follow-up

Phase 2 (proper RRWeb Click with Flutter widget info) is tracked separately and requires native additions across all three repos.

Made with Cursor

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>
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner June 13, 2026 01:19
* 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

@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.

Stale Bugbot comment from a previous run.

abelonogov-ld and others added 3 commits September 13, 2026 14:45
* 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>

@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.

Stale Bugbot comment from a previous run.

abelonogov-ld and others added 8 commits September 14, 2026 16:14
…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>

@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 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

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 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
abelonogov-ld enabled auto-merge (squash) September 15, 2026 21:27
@abelonogov-ld abelonogov-ld changed the title feat(flutter): record screen views (Navigate) from Dart feat(flutter): Screen views (Navigate) and Rich clicks Sep 15, 2026
@abelonogov-ld abelonogov-ld changed the title feat(flutter): Screen views (Navigate) and Rich clicks feat(flutter): Screen views (Navigate) and Rich Clicks Sep 15, 2026
@abelonogov-ld
abelonogov-ld merged commit cdd2190 into main Sep 15, 2026
28 checks passed
@abelonogov-ld
abelonogov-ld deleted the andrey/flutter-sr-screenview branch September 15, 2026 21:44
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 -->
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>
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.

2 participants