feat(sr): add hybrid Session Replay support on Android - #1129
feat(sr): add hybrid Session Replay support on Android#1129JuanNaranjoDD wants to merge 2 commits into
Conversation
Record a Flutter view embedded in a native Android host into the host's replay instead of a standalone session, mirroring the iOS support. Add FlutterSessionReplayManager, a process-wide singleton owning the feature, core and engine registry, and make FlutterSessionReplayBridge per-engine so multiple engines share one feature. Route embedded segments and resources through _SessionReplayInternalProxy, reached via compileOnly and a guarded Class.forName check so pure-Flutter apps degrade instead of crashing. Expose enableSessionReplay() on FlutterFragment/FlutterActivity/FlutterView.
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7bcd24ac2a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| manager.feature?.resourceResolver?.addResource( | ||
| resourceKey = resourceId, |
There was a problem hiding this comment.
Namespace resource keys by Flutter engine
When two engines record images, both Dart isolates allocate resource keys from the same startingResourceKey (lib/src/capture/recorder.dart), but these calls put them into the single manager-owned resolver whose map is keyed only by that integer. If both engines capture their first image before resolution, the second addResource overwrites the first entry, so the first engine can resolve and record the other engine's image hash. Keep a resolver per bridge or include the engine identity in the resolver key.
Useful? React with 👍 / 👎.
| val slotId = existing?.slotId ?: UUID.randomUUID().toString() | ||
| SlotRegistration(slotId, view).also { slotsByMessenger[messenger] = it } | ||
| } ?: return | ||
|
|
||
| embeddedSessionReplay.setSlotId(view, registration.slotId) |
There was a problem hiding this comment.
Flush buffered segments when registering the host slot
For a pre-warmed engine, setEmbedded and bind normally complete before the host calls enableSessionReplay(), and segments captured in between remain buffered. registerSlot makes the destination resolvable but does not notify the bridge; the only remaining flush trigger is a later writeSegment. If the UI stays static or the engine detaches before another segment is produced, those initial segments are never delivered. Trigger the bound bridge's pending-segment flush after installing the slot.
Useful? React with 👍 / 👎.
| override fun onFlutterEngineDetachedFromFlutterView() { | ||
| // The engine is already gone by the time this fires, so the slot cannot be | ||
| // unregistered by messenger here. It is dropped when the plugin detaches, and until | ||
| // then the weakly held view lets a re-attach reuse the same slot. |
There was a problem hiding this comment.
Unregister the slot when a cached engine leaves its view
When a cached FlutterEngine detaches from a FlutterView, its plugin remains attached to the engine, so DatadogSessionReplayPlugin.onDetachedFromEngine does not run. Leaving this callback empty therefore keeps the old view and slot resolvable, and the bridge continues sending records to a slot for which the native recorder no longer emits a visible placeholder until another view attaches. Retain the messenger supplied by the attachment callback and call unregisterSlot here.
Useful? React with 👍 / 👎.
fuzzybinary
left a comment
There was a problem hiding this comment.
I think Codex's comments might be worth addressing. Also, once it's ready I'd like @jonathanmos to take a look - specifically at the need for the extra GSON work. I'd love to avoid that if possible.
What and why?
Adds hybrid (add-to-app) Session Replay support on Android, mirroring the iOS support added in #1100.
When a Flutter module runs inside a native Android host, Session Replay currently records it as its own standalone session, disconnected from the host's replay. This change lets the Flutter view be recorded into the host application's replay instead, so a single session shows the native and Flutter UI composited together.
How?
Process-wide ownership.
FlutterSessionReplayManager.sharedowns the single feature, core, engine registry, and slot registry.FlutterSessionReplayBridgebecomes a per-engine class (keyed by an engine token) rather than an object, somultiple Flutter engines in one process share one feature and one context broadcast instead of fighting over it.
Ordering invariant. The player can only composite a Flutter record into a placeholder wireframe that already exists in the same segment. Slot IDs are therefore minted only in
registerSlotand never on read, and segments buffer inpendingSegments(capped) while the embedding state is stillUNKNOWN, so nothing is emitted before its placeholder can exist. The native side holds records until the placeholder is written.Host API.
enableSessionReplay()is exposed onFlutterFragment,FlutterActivity, andFlutterView, re-registering the slot onON_START.Feature names. The Flutter features are renamed to
flutter-session-replayandflutter-session-replay-resources, matching iOS, so registering them no longer evicts the native Session Replay module from the core.Review checklist