Skip to content

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

Open
mantrakp04 wants to merge 1 commit intodevfrom
chore/sentry-errs-stuff-shi
Open

fix(dashboard): improve analytics replay replayer lifecycle#1349
mantrakp04 wants to merge 1 commit intodevfrom
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

  • Bug Fixes
    • Improved stability of replay playback by enhancing detection and handling of disconnected replay instances.
    • Fixed error handling during replay initialization to prevent playback failures.
    • Enhanced replay controls to more reliably manage multiple replays and state transitions.

- 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 18, 2026 7:35pm
stack-backend Ready Ready Preview, Comment Apr 18, 2026 7:35pm
stack-dashboard Ready Ready Preview, Comment Apr 18, 2026 7:35pm
stack-demo Ready Ready Preview, Comment Apr 18, 2026 7:35pm
stack-docs Ready Ready Preview, Comment Apr 18, 2026 7:35pm
stack-preview-backend Ready Ready Preview, Comment Apr 18, 2026 7:35pm
stack-preview-dashboard Ready Ready Preview, Comment Apr 18, 2026 7:35pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

The PR adds an AGENTS.md file and refactors the rrweb replayer lifecycle management in the session replays page by introducing utility functions to detect and handle stale replayer instances, improving resource cleanup and preventing operations on disconnected replayers.

Changes

Cohort / File(s) Summary
New metadata file
AGENTS.md
Single-line file added without trailing newline.
Replayer lifecycle improvements
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Added stale replayer detection with isReplayerStale(). Introduced disposeReplayerForTab(), restartReplayerForTab(), and getUsableReplayerForTab() utility functions to manage replayer initialization, cleanup, and reuse. Updated state machine effects to handle stale replayers and skip invalid operations. Added rr-block body class toggling and disabled replayer warnings during initialization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #1187: Directly extends the rrweb replayer logic introduced in the session replays feature by adding robust lifecycle management and stale instance detection.

Suggested reviewers

  • N2D4

Poem

🐰 A replayer's life was fraught with strife,
When iframes vanished from sight,
But now with care, and functions fair,
Stale instances are handled right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
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.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(dashboard): improve analytics replay replayer lifecycle' clearly and concisely summarizes the main change, which is improving the reliability of the session replay viewer's replayer lifecycle management.
Description check ✅ Passed The PR description is complete and well-structured with a clear Summary section, detailed Changes list, a Test plan with manual verification steps, and Notes about additional changes included in the commit.

✏️ 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 improves the session replay viewer's reliability by introducing a isReplayerStale() check against rrweb's internal DOM properties and a consolidated disposeReplayerForTab / restartReplayerForTab / getUsableReplayerForTab helper set, wiring staleness detection into every executeEffects branch. It also adds an rr-block class to document.body to suppress recording of the admin dashboard while a replay is open, and consolidates previously duplicated teardown code.

  • The rr-block cleanup unconditionally removes the class on unmount, which can silently disable masking set by an external SDK (see inline comment).
  • disposeReplayerForTab does not clear the container's innerHTML before rescheduling init; rrweb appends to the container, so stale DOM nodes may remain visible during a mid-session restart.

Confidence Score: 5/5

Safe to merge; all findings are P2 quality-of-life improvements that don't block the primary replay flow.

No P0 or P1 correctness bugs found. The three comments address defensive edge cases that are unlikely to manifest in normal use and were pre-existing concerns in the original code. The core staleness detection and lifecycle cleanup logic is sound.

page-client.tsx around the disposeReplayerForTab and rr-block useEffect changes.

Important Files Changed

Filename Overview
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx Adds replayer lifecycle management: isReplayerStale(), disposeReplayerForTab(), restartReplayerForTab(), and getUsableReplayerForTab() helpers; wires staleness checks into all executeEffects cases; adds rr-block body class on mount; three P2 findings noted.
CLAUDE.md Converted from a plain file to a symlink pointing to AGENTS.md; target file exists in the repo.

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 / set_replayer_speed / set_replayer_skip_inactive]
    B --> E[recreate_replayer]

    C --> F[getUsableReplayerForTab]
    F --> G{isReplayerStale?}
    G -- No --> H[Return replayer and call play/pause]
    G -- Yes --> I[restartReplayerForTab]
    H --> J{throws?}
    J -- Yes --> I

    D --> K{isReplayerStale?}
    K -- Yes --> L[disposeReplayerForTab]
    K -- No --> M[Apply config / pause]

    E --> N[disposeReplayerForTab pause if exists]
    N --> O[pendingInitByTabRef.add]
    O --> P[ensureReplayerForTab async]

    I --> Q[disposeReplayerForTab scheduleReinit=true]
    Q --> R[pendingInitByTabRef.add]
    R --> S{container connected and has events?}
    S -- Yes --> P
    S -- No --> T[Wait for setContainerRefForTab]

    P --> U[new Replayer created]
    U --> V[REPLAYER_READY dispatched]
    V --> A
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 477-482

Comment:
**`rr-block` removal may conflict with other consumers**

`document.body.classList.remove("rr-block")` unconditionally removes the class on unmount, but doesn't check whether the class was already present before this component mounted. If another component (or the app's own session-recording SDK) independently adds `rr-block` to `<body>`, unmounting this page will strip the class they set, breaking their masking.

A safer approach is to only remove the class if this component was the one that added it:

```ts
useEffect(() => {
  const alreadyBlocked = document.body.classList.contains("rr-block");
  if (!alreadyBlocked) document.body.classList.add("rr-block");
  return () => {
    if (!alreadyBlocked) document.body.classList.remove("rr-block");
  };
}, []);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 75-84

Comment:
**Stale replayer detection relies on private rrweb internals**

`isReplayerStale` accesses `replayer.iframe` and `replayer.wrapper` via `as any` cast — these are undocumented internal properties of rrweb's `Replayer`. If a future rrweb upgrade renames or removes them, the function will silently return `false` for every replayer (both fields resolve to `null` → checked as `null`), disabling the entire staleness-detection and cleanup path introduced in this PR.

Consider adding an explicit version pin or a narrow unit test that asserts these fields exist, so a breaking rrweb upgrade is caught before it reaches production.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 842-849

Comment:
**Stale replayer DOM is not cleared before re-init**

`disposeReplayerForTab` removes the replayer from internal maps and disconnects the `ResizeObserver`, but does **not** clear the container element's innerHTML. When `scheduleReinit: true` and `ensureReplayerForTab` runs, rrweb's `Replayer` constructor appends a new `<iframe>` and wrapper to the same root, leaving the old replayer's DOM nodes in place.

The full-reset path (`destroyReplayers`) does clear innerHTML, but the per-tab restart path does not. This can result in duplicate iframes inside a tab container during a mid-session stale restart.

Adding `root.innerHTML = ""` before scheduling the reinit in `disposeReplayerForTab` when `scheduleReinit: true` would prevent this.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Enhance replay functionality in analytic..." | Re-trigger Greptile

Comment on lines +477 to +482
useEffect(() => {
document.body.classList.add("rr-block");
return () => {
document.body.classList.remove("rr-block");
};
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 rr-block removal may conflict with other consumers

document.body.classList.remove("rr-block") unconditionally removes the class on unmount, but doesn't check whether the class was already present before this component mounted. If another component (or the app's own session-recording SDK) independently adds rr-block to <body>, unmounting this page will strip the class they set, breaking their masking.

A safer approach is to only remove the class if this component was the one that added it:

useEffect(() => {
  const alreadyBlocked = document.body.classList.contains("rr-block");
  if (!alreadyBlocked) document.body.classList.add("rr-block");
  return () => {
    if (!alreadyBlocked) document.body.classList.remove("rr-block");
  };
}, []);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 477-482

Comment:
**`rr-block` removal may conflict with other consumers**

`document.body.classList.remove("rr-block")` unconditionally removes the class on unmount, but doesn't check whether the class was already present before this component mounted. If another component (or the app's own session-recording SDK) independently adds `rr-block` to `<body>`, unmounting this page will strip the class they set, breaking their masking.

A safer approach is to only remove the class if this component was the one that added it:

```ts
useEffect(() => {
  const alreadyBlocked = document.body.classList.contains("rr-block");
  if (!alreadyBlocked) document.body.classList.add("rr-block");
  return () => {
    if (!alreadyBlocked) document.body.classList.remove("rr-block");
  };
}, []);
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +75 to +84
function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
if (!replayer) return true;
const candidate = replayer as any;
const iframe = (candidate.iframe ?? null) as HTMLIFrameElement | null;
const wrapper = (candidate.wrapper ?? null) as HTMLElement | null;
if (!iframe || !iframe.isConnected) return true;
if (!iframe.contentDocument || !iframe.contentWindow) return true;
if (wrapper && !wrapper.isConnected) return true;
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Stale replayer detection relies on private rrweb internals

isReplayerStale accesses replayer.iframe and replayer.wrapper via as any cast — these are undocumented internal properties of rrweb's Replayer. If a future rrweb upgrade renames or removes them, the function will silently return false for every replayer (both fields resolve to null → checked as null), disabling the entire staleness-detection and cleanup path introduced in this PR.

Consider adding an explicit version pin or a narrow unit test that asserts these fields exist, so a breaking rrweb upgrade is caught before it reaches production.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 75-84

Comment:
**Stale replayer detection relies on private rrweb internals**

`isReplayerStale` accesses `replayer.iframe` and `replayer.wrapper` via `as any` cast — these are undocumented internal properties of rrweb's `Replayer`. If a future rrweb upgrade renames or removes them, the function will silently return `false` for every replayer (both fields resolve to `null` → checked as `null`), disabling the entire staleness-detection and cleanup path introduced in this PR.

Consider adding an explicit version pin or a narrow unit test that asserts these fields exist, so a breaking rrweb upgrade is caught before it reaches production.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +842 to +849
if (!scheduleReinit) return;

pendingInitByTabRef.current.add(tabKey);
const container = containerByTabRef.current.get(tabKey) ?? null;
const hasEvents = (eventsByTabRef.current.get(tabKey)?.length ?? 0) > 0;
if (!container || !container.isConnected || !hasEvents) return;

runAsynchronously(() => ensureReplayerForTab(tabKey, msRef.current.generation), { noErrorLogging: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Stale replayer DOM is not cleared before re-init

disposeReplayerForTab removes the replayer from internal maps and disconnects the ResizeObserver, but does not clear the container element's innerHTML. When scheduleReinit: true and ensureReplayerForTab runs, rrweb's Replayer constructor appends a new <iframe> and wrapper to the same root, leaving the old replayer's DOM nodes in place.

The full-reset path (destroyReplayers) does clear innerHTML, but the per-tab restart path does not. This can result in duplicate iframes inside a tab container during a mid-session stale restart.

Adding root.innerHTML = "" before scheduling the reinit in disposeReplayerForTab when scheduleReinit: true would prevent this.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx
Line: 842-849

Comment:
**Stale replayer DOM is not cleared before re-init**

`disposeReplayerForTab` removes the replayer from internal maps and disconnects the `ResizeObserver`, but does **not** clear the container element's innerHTML. When `scheduleReinit: true` and `ensureReplayerForTab` runs, rrweb's `Replayer` constructor appends a new `<iframe>` and wrapper to the same root, leaving the old replayer's DOM nodes in place.

The full-reset path (`destroyReplayers`) does clear innerHTML, but the per-tab restart path does not. This can result in duplicate iframes inside a tab container during a mid-session stale restart.

Adding `root.innerHTML = ""` before scheduling the reinit in `disposeReplayerForTab` when `scheduleReinit: true` would prevent this.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

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

Comment on lines +75 to +84
function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
if (!replayer) return true;
const candidate = replayer as any;
const iframe = (candidate.iframe ?? null) as HTMLIFrameElement | null;
const wrapper = (candidate.wrapper ?? null) as HTMLElement | null;
if (!iframe || !iframe.isConnected) return true;
if (!iframe.contentDocument || !iframe.contentWindow) return true;
if (wrapper && !wrapper.isConnected) return true;
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm rrweb's Replayer publicly exposes `iframe` and `wrapper` so the cast isn't needed.
fd -t f -e d.ts . node_modules/rrweb 2>/dev/null | head -20
rg -nP --type=ts -C2 '\b(iframe|wrapper)\s*:\s*HTML' -g '**/rrweb/**/*.d.ts' 2>/dev/null | head -40
# Also verify current usage in the file under review assumes these fields without casts
rg -nP --type=tsx -C1 'replayer\.(iframe|wrapper)' 'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/replays/page-client.tsx'

Repository: stack-auth/stack-auth

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Search for rrweb type definitions
echo "=== Looking for rrweb type definitions ==="
find node_modules/rrweb -name "*.d.ts" 2>/dev/null | head -10

echo -e "\n=== Searching for 'iframe' and 'wrapper' in rrweb types ==="
find node_modules/rrweb -name "*.d.ts" -exec grep -l "iframe\|wrapper" {} \; 2>/dev/null

echo -e "\n=== Checking RrwebReplayer usage in the file at lines 714-720 ==="
sed -n '710,725p' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/analytics/replays/page-client.tsx

Repository: stack-auth/stack-auth

Length of output: 830


🏁 Script executed:

#!/bin/bash
# Search for all usage of replayer.iframe or replayer.wrapper in the file (without type casts)
echo "=== All uses of replayer.iframe or replayer.wrapper in page-client.tsx ==="
rg -n 'replayer\.(iframe|wrapper)' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/analytics/replays/page-client.tsx -A2 -B2

Repository: stack-auth/stack-auth

Length of output: 1669


🏁 Script executed:

#!/bin/bash
# Find where RrwebReplayer is imported/defined
echo "=== RrwebReplayer import/definition ==="
rg -n 'RrwebReplayer|import.*Replayer|from.*rrweb' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/analytics/replays/page-client.tsx | head -20

# Check the isReplayerStale function to see the full context
echo -e "\n=== isReplayerStale function (lines 75-84) ==="
sed -n '75,85p' apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/analytics/replays/page-client.tsx

Repository: stack-auth/stack-auth

Length of output: 916


Remove as any — the RrwebReplayer type properly exposes these fields.

The RrwebReplayer type (imported from rrweb.Replayer at line 49) is fully typed. Throughout the file (lines 714–764), replayer.iframe and replayer.wrapper are accessed directly without any casts, confirming these properties are publicly available with proper types. The as any bypass violates the repo's guideline: "Do NOT use as/any/type casts or anything else like that to bypass the type system."

Additionally, replace the truthy checks (!replayer, !iframe, !wrapper) with explicit null checks (== null, != null) per the guideline preferring explicit null/undefinedness checks.

Refactor
 function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
-  if (!replayer) return true;
-  const candidate = replayer as any;
-  const iframe = (candidate.iframe ?? null) as HTMLIFrameElement | null;
-  const wrapper = (candidate.wrapper ?? null) as HTMLElement | null;
-  if (!iframe || !iframe.isConnected) return true;
-  if (!iframe.contentDocument || !iframe.contentWindow) return true;
-  if (wrapper && !wrapper.isConnected) return true;
+  if (replayer == null) return true;
+  const iframe = replayer.iframe;
+  const wrapper = replayer.wrapper;
+  if (iframe == null || !iframe.isConnected) return true;
+  if (iframe.contentDocument == null || iframe.contentWindow == null) return true;
+  if (wrapper != null && !wrapper.isConnected) return true;
   return false;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
if (!replayer) return true;
const candidate = replayer as any;
const iframe = (candidate.iframe ?? null) as HTMLIFrameElement | null;
const wrapper = (candidate.wrapper ?? null) as HTMLElement | null;
if (!iframe || !iframe.isConnected) return true;
if (!iframe.contentDocument || !iframe.contentWindow) return true;
if (wrapper && !wrapper.isConnected) return true;
return false;
}
function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
if (replayer == null) return true;
const iframe = replayer.iframe;
const wrapper = replayer.wrapper;
if (iframe == null || !iframe.isConnected) return true;
if (iframe.contentDocument == null || iframe.contentWindow == null) return true;
if (wrapper != null && !wrapper.isConnected) return true;
return false;
}
🤖 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 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.

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.

Comment on lines +75 to +83
function isReplayerStale(replayer: RrwebReplayer | null | undefined) {
if (!replayer) return true;
const candidate = replayer as any;
const iframe = (candidate.iframe ?? null) as HTMLIFrameElement | null;
const wrapper = (candidate.wrapper ?? null) as HTMLElement | null;
if (!iframe || !iframe.isConnected) return true;
if (!iframe.contentDocument || !iframe.contentWindow) return true;
if (wrapper && !wrapper.isConnected) return true;
return false;
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

isReplayerStale casts the replayer to any to access iframe/wrapper, but this file already uses replayer.iframe and replayer.wrapper with proper typing in ensureReplayerForTab. Avoiding the any cast here would preserve type-safety and catch API changes at compile time (e.g. access replayer.iframe / replayer.wrapper directly after the null check).

Copilot uses AI. Check for mistakes.
Comment on lines +815 to +821
const disposeReplayerForTab = useCallback((tabKey: TabKey, options?: {
pause?: boolean,
scheduleReinit?: boolean,
}) => {
const pause = options?.pause ?? true;
const scheduleReinit = options?.scheduleReinit ?? false;

Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

disposeReplayerForTab removes the replayer from the refs, but it doesn’t clear the speedSubRef subscription when disposing the active tab’s replayer. If the active replayer is disposed due to staleness (e.g. via pause_all) without a uiVersion change, the existing speedService.subscribe callback can remain attached to the old replayer and keep updating state. Consider explicitly unsubscribing (and nulling) speedSubRef when disposing msRef.current.activeTabKey (or otherwise triggering the skip-indicator effect to rerun).

Copilot uses AI. Check for mistakes.
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