Skip to content

feat(sr): add hybrid Session Replay support on Android - #1129

Draft
JuanNaranjoDD wants to merge 2 commits into
developfrom
juan.naranjo/hybrid-session-replay-android
Draft

feat(sr): add hybrid Session Replay support on Android#1129
JuanNaranjoDD wants to merge 2 commits into
developfrom
juan.naranjo/hybrid-session-replay-android

Conversation

@JuanNaranjoDD

Copy link
Copy Markdown
Contributor

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.shared owns the single feature, core, engine registry, and slot registry. FlutterSessionReplayBridge becomes a per-engine class (keyed by an engine token) rather than an object, so
multiple 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 registerSlot and never on read, and segments buffer in pendingSegments (capped) while the embedding state is still UNKNOWN, so nothing is emitted before its placeholder can exist. The native side holds records until the placeholder is written.

Host API. enableSessionReplay() is exposed on FlutterFragment, FlutterActivity, and FlutterView, re-registering the slot on ON_START.

Feature names. The Flutter features are renamed to flutter-session-replay and flutter-session-replay-resources, matching iOS, so registering them no longer evicts the native Session Replay module from the core.

Review checklist

  • This pull request has appropriate unit and / or integration tests
  • This pull request references a Github or JIRA issue

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.
@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Aug 24, 2026

Copy link
Copy Markdown

Pipelines

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

DataDog/dd-sdk-flutter | build-android — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d4a60a5 | Docs | View more details | Give us feedback!

@fuzzybinary

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment on lines +298 to 299
manager.feature?.resourceResolver?.addResource(
resourceKey = resourceId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +248 to +252
val slotId = existing?.slotId ?: UUID.randomUUID().toString()
SlotRegistration(slotId, view).also { slotsByMessenger[messenger] = it }
} ?: return

embeddedSessionReplay.setSlotId(view, registration.slotId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +70 to +73
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 fuzzybinary left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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