feat(flutter): record screen views (Navigate) from Dart#627
feat(flutter): record screen views (Navigate) from Dart#627abelonogov-ld wants to merge 4 commits into
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>
| super.didPop(route, previousRoute); | ||
| // After a pop the previously-underlying route becomes visible again, so it | ||
| // is the screen the user navigates back to. | ||
| _record(previousRoute); |
There was a problem hiding this comment.
Pop emits duplicate screen views
Medium Severity
LDNavigatorObserver always calls trackScreenView for previousRoute on every didPop, even when the popped route was skipped on didPush because it had no screen name. Dismissing unnamed overlays (typical dialogs and bottom sheets) therefore emits an extra Navigate / screen_view for the route already underneath, duplicating the prior screen view in Session Replay and analytics.
Reviewed by Cursor Bugbot for commit ca47068. Configure here.
| // After a pop the previously-underlying route becomes visible again, so it | ||
| // is the screen the user navigates back to. | ||
| _record(previousRoute); | ||
| } |
There was a problem hiding this comment.
Route removal skips screen tracking
Low Severity
Auto-capture listens only to didPush, didReplace, and didPop. When the top route is removed with Navigator.removeRoute (or similar APIs that notify didRemove instead of didPop), the newly visible route never triggers trackScreenView, so Session Replay and spans miss that navigation.
Reviewed by Cursor Bugbot for commit ca47068. Configure here.
* 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
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 16c0a46. Configure here.
| debugPrint: options.instrumentation.debugPrint, | ||
| ), | ||
| trackEventsEnabled: options.analytics.trackEvents, | ||
| pageViewsEnabled: options.analytics.pageViews, |
There was a problem hiding this comment.
Wrong analytics flag wired
High Severity
configFromOptions sets pageViewsEnabled from options.analytics.pageViews, but AnalyticsOptions only defines views (documented as the screen-view gate). Web/stub trackScreenView therefore ignores analytics.views, so disabling screen views via the public options may have no effect.
Reviewed by Cursor Bugbot for commit 16c0a46. Configure here.
| s.dependency 'LaunchDarklyObservability', '~> 0.46.0' | ||
| s.dependency 'LaunchDarklySessionReplay', '~> 0.46.0' | ||
| s.dependency 'LaunchDarklyObservability', '~> 0.44.0' | ||
| s.dependency 'LaunchDarklySessionReplay', '~> 0.44.0' |
There was a problem hiding this comment.
iOS CocoaPods version downgraded
High Severity
The podspec pins LaunchDarklyObservability and LaunchDarklySessionReplay to ~> 0.44.0 while Swift Package Manager in the same PR requires 0.46.1. CocoaPods iOS builds can resolve an older native SDK or conflict with apps already on 0.46.x, breaking trackScreenView integration.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 16c0a46. Configure here.


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