Skip to content

fix(dashboard): improve analytics replay replayer lifecycle#1349

Merged
mantrakp04 merged 6 commits into
devfrom
chore/sentry-errs-stuff-shi
Apr 28, 2026
Merged

fix(dashboard): improve analytics replay replayer lifecycle#1349
mantrakp04 merged 6 commits into
devfrom
chore/sentry-errs-stuff-shi

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented Apr 18, 2026

Summary

Improves reliability of the session replay viewer on the project analytics replays page by tracking replayer staleness, coordinating pause/restart with effects, and cleaning up instances to avoid leaks.

Changes

  • Add isReplayerStale and wire replayer lifecycle into executeEffects so playback and pause stay in sync with the replayer state.
  • Pause/restart and teardown when the replayer becomes stale or unmounts.

Test plan

  • Open a project’s Analytics → Replays, load a replay, scrub timeline, pause/resume, and switch replays; confirm no stuck playback or console errors.
  • pnpm lint / pnpm typecheck on touched packages if CI does not cover.

Notes

Small CLAUDE.md tweak included in the same commit.

Made with Cursor

Summary by CodeRabbit

  • Chores
    • Disabled automatic session recording in the dashboard.

- Introduced `isReplayerStale` function to check the state of the replayer.
- Added effects to manage the replayer lifecycle, including pausing and restarting based on its state.
- Updated `executeEffects` to utilize the new replayer management logic, ensuring smoother playback and pause operations.
- Improved cleanup of replayer instances to prevent memory leaks and ensure proper resource management.

This update enhances the reliability and performance of the replay feature in the analytics section.
Copilot AI review requested due to automatic review settings April 18, 2026 19:30
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-backend Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-dashboard Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-demo Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-docs Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-preview-backend Ready Ready Preview, Comment Apr 24, 2026 5:04pm
stack-preview-dashboard Ready Ready Preview, Comment Apr 24, 2026 5:04pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 837686fb-69d9-4f1c-8228-d050dcd49936

📥 Commits

Reviewing files that changed from the base of the PR and between 6d634a9 and 4bf3042.

📒 Files selected for processing (1)
  • apps/dashboard/instrumentation-client.ts

📝 Walkthrough

Walkthrough

The PostHog client initialization in the instrumentation client has been updated to disable automatic web session recording by replacing the session_recording configuration with disable_session_recording: true, affecting session recording behavior while keeping the rest of the Sentry and replay configuration intact.

Changes

Cohort / File(s) Summary
PostHog Session Recording Configuration
apps/dashboard/instrumentation-client.ts
Disabled automatic web session recording by replacing session_recording config with disable_session_recording: true. Sentry initialization and replay integration remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Suggested reviewers

  • N2D4
  • nams1570

Poem

🐰 A session's rest, the recording stops,
PostHog now listens less, the config hops!
No more auto-capture in the dark,
Just clean disable—a rabbit's spark! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title refers to improving replayer lifecycle management, but the raw_summary shows only PostHog session recording configuration changes, creating a mismatch between title and actual file changes. Update the title to accurately reflect the actual change: disabling PostHog session recording to avoid conflicts with Sentry Replay, or provide context showing how this config change improves replayer lifecycle.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description details session replay reliability improvements with extensive test plans and lifecycle management, but the raw_summary shows only a PostHog configuration change, indicating a significant disconnect between described and actual changes. Clarify whether the raw_summary is incomplete or whether the description describes changes beyond the single configuration file modified. Ensure description aligns with actual code changes shown in the raw_summary.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/sentry-errs-stuff-shi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR refactors the session replay lifecycle in the analytics dashboard by introducing isReplayerStale (detecting disconnected rrweb iframes), centralizing dispose/restart logic into disposeReplayerForTab/restartReplayerForTab/getUsableReplayerForTab, and wiring staleness checks throughout executeEffects. Additional clean-ups include the rr-block guard fix, clearing orphaned iframe DOM on re-init (root.innerHTML = ""), and extracting the SessionReplayEventsResponse named type.

Confidence Score: 5/5

Safe to merge — the lifecycle refactor is well-structured with no new regressions introduced.

No new P0 or P1 issues found. The staleness detection, dispose/restart helpers, and snapshot-before-mutate iteration pattern are all implemented correctly. The rr-block guard and root.innerHTML clearing (for the scheduleReinit: true path) address the core concerns from prior review rounds. Remaining open items (private rrweb internals, DOM clearing in the recreate_replayer code path) are pre-existing P2 concerns already captured in previous threads.

No files require special attention beyond the open previous-thread items in page-client.tsx.

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx Core replay lifecycle overhauled: staleness detection, dispose/restart helpers, and executeEffects now route all replayer interactions through staleness guards. Snapshot iteration ([...map.entries()]) prevents mid-loop mutation issues. Two previously flagged concerns remain open: reliance on private rrweb internals (replayer.iframe/replayer.wrapper) and the lack of root.innerHTML clearing in the recreate_replayer code path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[executeEffects] --> B{effect type}
    B --> C[play_replayer / pause_replayer_at / sync_mini_tabs]
    B --> D[pause_all]
    B --> E[set_replayer_speed / set_replayer_skip_inactive]
    B --> F[recreate_replayer]

    C --> G[getUsableReplayerForTab]
    G --> H{isReplayerStale?}
    H -- No --> I[return replayer]
    H -- Yes --> J[restartReplayerForTab]
    J --> K[disposeReplayerForTab\nscheduleReinit=true]
    K --> L[root.innerHTML cleared]
    L --> M[runAsynchronously\nensureReplayerForTab]
    G -- null --> N[ensureReplayerForTab directly]

    D --> O{isReplayerStale?}
    O -- Yes --> P[disposeReplayerForTab\nno reinit]
    O -- No --> Q[replayer.pause]
    Q -- throws --> J

    E --> R{isReplayerStale?}
    R -- Yes --> J
    R -- No --> S[replayer.setConfig]
    S -- throws --> J

    F --> T[disposeReplayerForTab\nscheduleReinit=false]
    T --> U[pendingInitByTabRef.add]
    U --> M
Loading

Reviews (4): Last reviewed commit: "fix replay replayer lifecycle cleanup" | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx (1)

1244-1249: Unmount cleanup still uses pre-staleness-check destroyReplayers.

The unmount effect calls the older destroyReplayers helper (lines 627-649), which calls r.pause() inside try/catch without first checking isReplayerStale. That's functionally fine (the catch absorbs errors on disconnected iframes), but it's inconsistent with the new pattern introduced in this PR where stale checks gate pause() calls.

For consistency and slightly cleaner logs/error traces, consider having destroyReplayers reuse disposeReplayerForTab({ pause: true, scheduleReinit: false }) for each tab, or at least guard the pause() call with isReplayerStale.

Not blocking — just a coherence nit so the two teardown paths follow the same discipline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 1244 - 1249, The unmount cleanup currently calls the older
destroyReplayers which pauses replayers without checking staleness; change the
teardown to follow the new pattern by iterating through tabs and calling
disposeReplayerForTab({ pause: true, scheduleReinit: false }) (or at minimum
check isReplayerStale before calling r.pause()) instead of using
destroyReplayers, so genCounterRef.current += 1; teardown uses the same
staleness-guarded disposal logic as other paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 75-84: The isReplayerStale function uses an unnecessary cast ("as
any") to access iframe and wrapper; remove the cast and access replayer.iframe
and replayer.wrapper directly (RrwebReplayer exposes these fields). Also replace
the truthy/falsy checks with explicit null/undefined checks: use == null / !=
null semantics (e.g., if (replayer == null) return true; treat iframe and
wrapper with explicit null checks and check isConnected only when they != null),
and keep the existing contentDocument/contentWindow checks accordingly to
preserve the original logic.

---

Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 1244-1249: The unmount cleanup currently calls the older
destroyReplayers which pauses replayers without checking staleness; change the
teardown to follow the new pattern by iterating through tabs and calling
disposeReplayerForTab({ pause: true, scheduleReinit: false }) (or at minimum
check isReplayerStale before calling r.pause()) instead of using
destroyReplayers, so genCounterRef.current += 1; teardown uses the same
staleness-guarded disposal logic as other paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84149ed5-fb10-4f5e-ba07-d94c15d95c59

📥 Commits

Reviewing files that changed from the base of the PR and between f4ca6cb and ed6961a.

📒 Files selected for processing (3)
  • CLAUDE.md
  • CLAUDE.md
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves the dashboard’s Analytics → Replays session replay viewer reliability by adding replayer “staleness” detection and centralizing per-tab replayer disposal/restart logic, then wiring this into the effect executor to better keep playback state and rrweb instance state in sync.

Changes:

  • Add isReplayerStale(...) and route rrweb play/pause/config effects through a “usable replayer” accessor that restarts stale instances.
  • Introduce disposeReplayerForTab / restartReplayerForTab helpers to coordinate teardown and re-init scheduling.
  • Adjust rrweb Replayer initialization (e.g. showWarning: false) and add a page-level rr-block body class during mount.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mantrakp04 mantrakp04 requested a review from N2D4 April 20, 2026 18:13
… on reinit

- Added a check to prevent adding the "rr-block" class if it already exists on the document body.
- Introduced a mechanism to clear any leftover DOM elements from the previous replayer instance to avoid stacking issues.
- Improved handling of replayer root references to ensure proper cleanup and initialization.
…er restart logic. This change ensures that when an error occurs during replayer operations (pause, setConfig), the replayer is restarted for the affected tab, improving stability and user experience.
@mantrakp04
Copy link
Copy Markdown
Collaborator Author

@greptile-ai review

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx (1)

994-1003: ⚠️ Potential issue | 🟠 Major

recreate_replayer effect can stack iframes inside the container.

disposeReplayerForTab(tabKey, { scheduleReinit: false }) only clears root.innerHTML when scheduleReinit is true (lines 846–850). Because recreate_replayer passes scheduleReinit: false and then manually schedules ensureReplayerForTab, the previous rrweb-rendered iframe/wrapper are left in the container DOM, and the new new Replayer(events, { root: rootEl, ... }) will append a second pair of children — exactly the stacking case the dispose helper comments warn about ("so the new one doesn't stack a second iframe/wrapper into the same container").

Either route this through the existing reinit path (preferred), or clear the root DOM explicitly before re-initialising.

🛠️ Proposed fix (explicit DOM clear before reinit)
         case "recreate_replayer": {
           const tabKey = effect.tabKey;
           disposeReplayerForTab(tabKey, {
             pause: replayerByTabRef.current.has(tabKey),
             scheduleReinit: false,
           });
+          const root = containerByTabRef.current.get(tabKey) ?? null;
+          if (root) root.innerHTML = "";
           pendingInitByTabRef.current.add(tabKey);
           runAsynchronously(() => ensureReplayerForTab(tabKey, effect.generation), { noErrorLogging: true });
           break;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
around lines 994 - 1003, The "recreate_replayer" effect currently calls
disposeReplayerForTab(tabKey, { scheduleReinit: false }) then schedules
ensureReplayerForTab, which leaves the previous rrweb iframe/wrapper in the
container and causes stacking; fix by routing through the existing reinit path
or explicitly clearing the container before re-init: either call
disposeReplayerForTab(tabKey, { scheduleReinit: true }) so the helper clears
root.innerHTML and schedules reinit, or immediately locate the tab's root
element (via replayerByTabRef/current lookup used by disposeReplayerForTab) and
set root.innerHTML = '' before invoking pendingInitByTabRef.current.add(tabKey)
and runAsynchronously(() => ensureReplayerForTab(tabKey, effect.generation), {
noErrorLogging: true }); ensure you update only the recreate_replayer branch to
prevent stacked iframes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx:
- Around line 994-1003: The "recreate_replayer" effect currently calls
disposeReplayerForTab(tabKey, { scheduleReinit: false }) then schedules
ensureReplayerForTab, which leaves the previous rrweb iframe/wrapper in the
container and causes stacking; fix by routing through the existing reinit path
or explicitly clearing the container before re-init: either call
disposeReplayerForTab(tabKey, { scheduleReinit: true }) so the helper clears
root.innerHTML and schedules reinit, or immediately locate the tab's root
element (via replayerByTabRef/current lookup used by disposeReplayerForTab) and
set root.innerHTML = '' before invoking pendingInitByTabRef.current.add(tabKey)
and runAsynchronously(() => ensureReplayerForTab(tabKey, effect.generation), {
noErrorLogging: true }); ensure you update only the recreate_replayer branch to
prevent stacked iframes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c440c8a5-d701-4bd7-920b-7134d0f9320f

📥 Commits

Reviewing files that changed from the base of the PR and between 8bae37b and 6d634a9.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx

…eplay handling

- Updated instrumentation-client to disable PostHog session recording to prevent conflicts with Sentry's Replay integration.
- Removed unused session replay event types and lifecycle management code from PageClient to streamline functionality.
- Enhanced error handling in PageClient by directly accessing replayer instances, improving stability during playback operations.
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 left a comment

Choose a reason for hiding this comment

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

lgtm

@mantrakp04 mantrakp04 merged commit 0207721 into dev Apr 28, 2026
34 checks passed
@mantrakp04 mantrakp04 deleted the chore/sentry-errs-stuff-shi branch April 28, 2026 22:29
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.

3 participants