diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx
index adf027db..6119936c 100644
--- a/packages/app/src/components/DBRowSidePanel.tsx
+++ b/packages/app/src/components/DBRowSidePanel.tsx
@@ -506,9 +506,11 @@ const DBRowSidePanel = ({
+
{
if (!subDrawerOpen) {
_onClose();
diff --git a/packages/app/tests/e2e/features/sessions.spec.ts b/packages/app/tests/e2e/features/sessions.spec.ts
index c3aba28a..aec7b09f 100644
--- a/packages/app/tests/e2e/features/sessions.spec.ts
+++ b/packages/app/tests/e2e/features/sessions.spec.ts
@@ -39,4 +39,81 @@ test.describe('Client Sessions Functionality', { tag: ['@sessions'] }, () => {
await sessionsPage.openFirstSession();
});
});
+
+ test(
+ 'clicking a session event opens the event detail panel with tabs, not another session replay',
+ { tag: ['@full-stack'] },
+ async ({ page }) => {
+ await test.step('Navigate and open a session (with sidePanelTab=replay pre-set in URL to simulate search-page flow)', async () => {
+ // Pre-set sidePanelTab=replay in the URL to simulate navigating from a search page
+ // row detail panel that had the Session Replay tab open. Without isNestedPanel=true,
+ // the inner DBRowSidePanel would inherit this URL param and open to the Replay tab again.
+ await page.goto('/search');
+ await sessionsPage.goto();
+ await sessionsPage.selectDataSource();
+ await expect(sessionsPage.getFirstSessionCard()).toBeVisible();
+ // Inject sidePanelTab=replay into the URL before opening the session
+ const currentUrl = page.url();
+ await page.goto(
+ currentUrl.includes('?')
+ ? `${currentUrl}&sidePanelTab=replay`
+ : `${currentUrl}?sidePanelTab=replay`,
+ );
+ await expect(sessionsPage.getFirstSessionCard()).toBeVisible();
+ await sessionsPage.openFirstSession();
+ });
+
+ await test.step('Wait for session replay drawer and event rows to load', async () => {
+ await expect(sessionsPage.sessionSidePanel).toBeVisible();
+ // Wait for the session event list to populate (routeChange/console.error events are seeded)
+ await expect(sessionsPage.getSessionEventRows().first()).toBeVisible({
+ timeout: 15000,
+ });
+ });
+
+ await test.step('Click a session event row', async () => {
+ await sessionsPage.clickFirstSessionEvent();
+ });
+
+ await test.step('Event detail panel opens alongside the session replay — not replacing it', async () => {
+ // The row-side-panel must be visible (event detail drawer opened on top of session replay)
+ await expect(sessionsPage.rowSidePanel).toBeVisible();
+
+ // The original session replay panel must still be open (not replaced/closed)
+ await expect(sessionsPage.sessionSidePanel).toBeVisible();
+
+ // Only one session-side-panel must exist (not a second replay opened inside the detail panel)
+ await expect(page.getByTestId('session-side-panel')).toHaveCount(1);
+
+ // The row-side-panel must show the event detail TabBar (Overview, Trace, etc.)
+ // This guards against the regression where the inner panel re-opened session replay
+ // instead of showing event details (which has no TabBar, just the replay player)
+ await expect(
+ sessionsPage.rowSidePanel.getByTestId('side-panel-tabs'),
+ ).toBeVisible();
+
+ // The inner panel must NOT be showing the Session Replay tab content.
+ // Without isNestedPanel=true (broken), the inner DBRowSidePanel reads sidePanelTab=replay
+ // from the URL (injected above) and renders the Session Replay tab content (side-panel-tab-replay).
+ // With isNestedPanel=true (fixed), the inner panel uses local state and ignores the URL,
+ // opening to its default tab (Trace/Overview) instead.
+ await expect(
+ sessionsPage.rowSidePanel.getByTestId('side-panel-tab-replay'),
+ ).toHaveCount(0);
+ });
+
+ await test.step('Clicking the overlay closes the event detail panel but keeps the session replay open', async () => {
+ // Without the fix, withOverlay={!isNestedPanel} removed the overlay on nested panels,
+ // so there was nothing to click to close the panel (it had to be ESC only).
+ // With the fix (withOverlay always true), clicking the Mantine overlay dismisses the inner panel.
+ await sessionsPage.clickTopmostDrawerOverlay();
+
+ // The event detail panel must close
+ await expect(sessionsPage.rowSidePanel).toBeHidden();
+
+ // The session replay drawer must still be open
+ await expect(sessionsPage.sessionSidePanel).toBeVisible();
+ });
+ },
+ );
});
diff --git a/packages/app/tests/e2e/page-objects/SessionsPage.ts b/packages/app/tests/e2e/page-objects/SessionsPage.ts
index 33d7d7e9..274f563a 100644
--- a/packages/app/tests/e2e/page-objects/SessionsPage.ts
+++ b/packages/app/tests/e2e/page-objects/SessionsPage.ts
@@ -68,6 +68,46 @@ export class SessionsPage {
await this.getFirstSessionCard().click();
}
+ /**
+ * Get the session side panel (the replay drawer)
+ */
+ get sessionSidePanel() {
+ return this.page.getByTestId('session-side-panel');
+ }
+
+ /**
+ * Get all session event rows inside the replay drawer
+ */
+ getSessionEventRows() {
+ return this.page.locator('[data-testid^="session-event-row-"]');
+ }
+
+ /**
+ * Click the first session event row to open its detail panel
+ */
+ async clickFirstSessionEvent() {
+ await this.getSessionEventRows().first().click();
+ }
+
+ /**
+ * Get the row side panel (event detail drawer opened from within session replay)
+ */
+ get rowSidePanel() {
+ return this.page.getByTestId('row-side-panel');
+ }
+
+ /**
+ * Click the Mantine overlay of the topmost open drawer to close it.
+ * Mantine renders one overlay per open Drawer. The last one belongs to
+ * the innermost (topmost) drawer.
+ */
+ async clickTopmostDrawerOverlay() {
+ // Mantine overlays are siblings of the drawer content inside the portal root.
+ // Use the last one since the inner panel's overlay is rendered on top.
+ const overlay = this.page.locator('.mantine-Drawer-overlay').last();
+ await overlay.click({ position: { x: 10, y: 10 } });
+ }
+
// Getters for assertions
get form() {
diff --git a/packages/app/tests/e2e/seed-clickhouse.ts b/packages/app/tests/e2e/seed-clickhouse.ts
index ee90a4db..33885611 100644
--- a/packages/app/tests/e2e/seed-clickhouse.ts
+++ b/packages/app/tests/e2e/seed-clickhouse.ts
@@ -321,6 +321,29 @@ function generateSessionTraces(
`('${timestampNs}', '${traceId}', '${spanId}', '', '', '${spanName}', 'SPAN_KIND_INTERNAL', 'browser', {'rum.sessionId':'${sessionId}','service.name':'browser'}, '', '', {'component':'${component}','page.url':'https://example.com/dashboard','teamId':'${teamId}','teamName':'${teamName}','userEmail':'${userEmail}','userName':'${userName}'}, 0, '${statusCode}', '', [], [], [], [], [], [], [])`,
);
}
+
+ // Add visible events for the session event list:
+ // - A routeChange (navigation) event — shown in both Highlighted and All Events tabs
+ // - A console.error event — shown in both tabs
+ const sessionUserIndex = i % 5;
+ const sessionUserEmail = `test${sessionUserIndex}@example.com`;
+ const sessionUserName = `Test User ${sessionUserIndex}`;
+ const sessionTeamId = 'test-team-id';
+ const sessionTeamName = 'Test Team';
+
+ const navTimestampNs = (baseTime - 1000) * 1000000;
+ const navTraceId = `session-nav-${i}`;
+ const navSpanId = `session-nav-span-${i}`;
+ rows.push(
+ `('${navTimestampNs}', '${navTraceId}', '${navSpanId}', '', '', 'routeChange', 'SPAN_KIND_INTERNAL', 'browser', {'rum.sessionId':'${sessionId}','service.name':'browser'}, '', '', {'component':'navigation','location.href':'https://example.com/dashboard','teamId':'${sessionTeamId}','teamName':'${sessionTeamName}','userEmail':'${sessionUserEmail}','userName':'${sessionUserName}'}, 0, 'STATUS_CODE_OK', '', [], [], [], [], [], [], [])`,
+ );
+
+ const errTimestampNs = (baseTime - 2000) * 1000000;
+ const errTraceId = `session-err-${i}`;
+ const errSpanId = `session-err-span-${i}`;
+ rows.push(
+ `('${errTimestampNs}', '${errTraceId}', '${errSpanId}', '', '', 'console.error', 'SPAN_KIND_INTERNAL', 'browser', {'rum.sessionId':'${sessionId}','service.name':'browser'}, '', '', {'component':'error','message':'E2E test error ${i}','teamId':'${sessionTeamId}','teamName':'${sessionTeamName}','userEmail':'${sessionUserEmail}','userName':'${sessionUserName}'}, 0, 'STATUS_CODE_ERROR', '', [], [], [], [], [], [], [])`,
+ );
}
return rows.join(',\n');