Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-monkeys-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

Fix bug when accessing session replay panel from search page
2 changes: 1 addition & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"eslint-plugin-playwright": "^2.4.0",
"eslint-plugin-react-hook-form": "^0.3.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-storybook": "10.1.4",
"eslint-plugin-storybook": "^10.3.3",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/SessionEventList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const EventRow = React.forwardRef(
return (
<div
data-index={dataIndex}
data-testid={`session-event-row-${dataIndex}`}
ref={ref}
className={cx(styles.eventRow, {
[styles.eventRowError]: event.isError,
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/SessionSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ export default function SessionSidePanel({
className="border-start"
>
<ZIndexContext.Provider value={zIndex}>
<div className="d-flex flex-column h-100">
<div
className="d-flex flex-column h-100"
data-testid="session-side-panel"
>
<div>
<div className="p-3 d-flex align-items-center justify-content-between border-bottom border-dark">
<div style={{ width: '50%', maxWidth: 500 }}>
Expand Down
24 changes: 15 additions & 9 deletions packages/app/src/SessionSubpanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {

import DBRowSidePanel from '@/components/DBRowSidePanel';
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import { useZIndex, ZIndexContext } from '@/zIndex';

import SearchWhereInput from './components/SearchInput/SearchWhereInput';
import useFieldExpressionGenerator from './hooks/useFieldExpressionGenerator';
Expand Down Expand Up @@ -267,6 +268,8 @@ export default function SessionSubpanel({
whereLanguage?: SearchConditionLanguage;
onLanguageChange?: (lang: 'sql' | 'lucene') => void;
}) {
const contextZIndex = useZIndex();

const [rowId, setRowId] = useState<string | undefined>(undefined);
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);

Expand Down Expand Up @@ -466,15 +469,18 @@ export default function SessionSubpanel({
<div className={styles.wrapper}>
{rowId != null && traceSource && (
<Portal>
<DBRowSidePanel
source={traceSource}
rowId={rowId}
aliasWith={aliasWith}
onClose={() => {
setDrawerOpen(false);
setRowId(undefined);
}}
/>
<ZIndexContext.Provider value={contextZIndex}>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was the actual file to fix the issue - the rest is adding/improving e2e tests

<DBRowSidePanel
source={traceSource}
rowId={rowId}
aliasWith={aliasWith}
isNestedPanel
onClose={() => {
setDrawerOpen(false);
setRowId(undefined);
}}
/>
</ZIndexContext.Provider>
</Portal>
)}
<div className={cx(styles.eventList, { 'd-none': playerFullWidth })}>
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/components/DBRowSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,11 @@ const DBRowSidePanel = ({
</div>
)}
>
<div className="overflow-hidden flex-grow-1">
<div
className="overflow-hidden flex-grow-1"
data-testid="side-panel-tab-replay"
>
<DBSessionPanel
data-testid="side-panel-tab-replay"
dateRange={fourHourRange}
focusDate={focusDate}
setSubDrawerOpen={setSubDrawerOpen}
Expand Down
64 changes: 64 additions & 0 deletions packages/app/tests/e2e/features/sessions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,68 @@ 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);
});
},
);
});
28 changes: 28 additions & 0 deletions packages/app/tests/e2e/page-objects/SessionsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ 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');
}

// Getters for assertions

get form() {
Expand Down
20 changes: 20 additions & 0 deletions packages/app/tests/e2e/seed-clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ 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 navTimestampNs = (baseTime - 1000) * 1000000;
const navTraceId = `session-nav-${i}`;
const navSpanId = `session-nav-span-${i}`;
const userIndex = i % 5;
const userEmail = `test${userIndex}@example.com`;
const userName = `Test User ${userIndex}`;
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':'test-team-id','teamName':'Test Team','userEmail':'${userEmail}','userName':'${userName}'}, 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':'test-team-id','teamName':'Test Team','userEmail':'${userEmail}','userName':'${userName}'}, 0, 'STATUS_CODE_ERROR', '', [], [], [], [], [], [], [])`,
);
}

return rows.join(',\n');
Expand Down
101 changes: 92 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4366,7 +4366,7 @@ __metadata:
eslint-plugin-playwright: "npm:^2.4.0"
eslint-plugin-react-hook-form: "npm:^0.3.1"
eslint-plugin-react-hooks: "npm:^7.0.1"
eslint-plugin-storybook: "npm:10.1.4"
eslint-plugin-storybook: "npm:^10.3.3"
flat: "npm:^5.0.2"
fuse.js: "npm:^6.6.2"
http-proxy-middleware: "npm:^3.0.5"
Expand Down Expand Up @@ -10340,6 +10340,19 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/project-service@npm:8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/project-service@npm:8.58.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.58.0"
"@typescript-eslint/types": "npm:^8.58.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10c0/e6d0cb2f7708ccb31a2ff9eb35817d4999c26e1f1cd3c607539e21d0c73a234daa77c73ee1163bc4e8b139252d619823c444759f1ddabdd138cab4885e9c9794
languageName: node
linkType: hard

"@typescript-eslint/scope-manager@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/scope-manager@npm:8.48.1"
Expand All @@ -10360,6 +10373,16 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/scope-manager@npm:8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/scope-manager@npm:8.58.0"
dependencies:
"@typescript-eslint/types": "npm:8.58.0"
"@typescript-eslint/visitor-keys": "npm:8.58.0"
checksum: 10c0/bd5c16780f22d62359af0f69909f38a15fa3c55e609124a7cd5c2a04322fe41e586d81066f3ad1dcc3c1eff24dbcb48b78d099626d611fbd680c20c005d48f1d
languageName: node
linkType: hard

"@typescript-eslint/tsconfig-utils@npm:8.48.1, @typescript-eslint/tsconfig-utils@npm:^8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.1"
Expand All @@ -10378,6 +10401,15 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/tsconfig-utils@npm:8.58.0, @typescript-eslint/tsconfig-utils@npm:^8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.58.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10c0/0a07fe1a28b2513e625882bc8d4c4e0c5a105cdbcb987beae12fc66dbe71dc9638013e4d1fa8ad10d828a2acd5e3fed987c189c00d41fed0e880009f99adf1b2
languageName: node
linkType: hard

"@typescript-eslint/type-utils@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/type-utils@npm:8.48.1"
Expand Down Expand Up @@ -10424,6 +10456,13 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/types@npm:8.58.0, @typescript-eslint/types@npm:^8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/types@npm:8.58.0"
checksum: 10c0/f2fe1321758a04591c20d77caba956ae76b77cff0b976a0224b37077d80b1ebd826874d15ec79c3a3b7d57ee5679e5d10756db1b082bde3d51addbd3a8431d38
languageName: node
linkType: hard

"@typescript-eslint/typescript-estree@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/typescript-estree@npm:8.48.1"
Expand Down Expand Up @@ -10462,7 +10501,26 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/utils@npm:8.48.1, @typescript-eslint/utils@npm:^8.8.1":
"@typescript-eslint/typescript-estree@npm:8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/typescript-estree@npm:8.58.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.58.0"
"@typescript-eslint/tsconfig-utils": "npm:8.58.0"
"@typescript-eslint/types": "npm:8.58.0"
"@typescript-eslint/visitor-keys": "npm:8.58.0"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
tinyglobby: "npm:^0.2.15"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10c0/a8cb94cb765b27740a54f9b5378bd8f0dc49e301ceed99a0791dc9d1f61c2a54e3212f7ed9120c8c2df80104ad3117150cf5e7fe8a0b7eec3ed04969a79b103e
languageName: node
linkType: hard

"@typescript-eslint/utils@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/utils@npm:8.48.1"
dependencies:
Expand Down Expand Up @@ -10492,6 +10550,21 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/utils@npm:^8.48.0":
version: 8.58.0
resolution: "@typescript-eslint/utils@npm:8.58.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.58.0"
"@typescript-eslint/types": "npm:8.58.0"
"@typescript-eslint/typescript-estree": "npm:8.58.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10c0/457e01a6e6d954dbfe13c49ece3cf8a55e5d8cf19ea9ae7086c0e205d89e3cdbb91153062ab440d2e78ad3f077b174adc42bfb1b6fc24299020a0733e7f9c11c
languageName: node
linkType: hard

"@typescript-eslint/visitor-keys@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/visitor-keys@npm:8.48.1"
Expand All @@ -10512,6 +10585,16 @@ __metadata:
languageName: node
linkType: hard

"@typescript-eslint/visitor-keys@npm:8.58.0":
version: 8.58.0
resolution: "@typescript-eslint/visitor-keys@npm:8.58.0"
dependencies:
"@typescript-eslint/types": "npm:8.58.0"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10c0/75f3c9c097a308cc6450822a0f81d44c8b79b524e99dd2c41ded347b12f148ab3bd459ce9cc6bd00f8f0725c5831baab6d2561596ead3394ab76dddbeb32cce1
languageName: node
linkType: hard

"@uiw/codemirror-extensions-basic-setup@npm:4.23.3":
version: 4.23.3
resolution: "@uiw/codemirror-extensions-basic-setup@npm:4.23.3"
Expand Down Expand Up @@ -15500,15 +15583,15 @@ __metadata:
languageName: node
linkType: hard

"eslint-plugin-storybook@npm:10.1.4":
version: 10.1.4
resolution: "eslint-plugin-storybook@npm:10.1.4"
"eslint-plugin-storybook@npm:^10.3.3":
version: 10.3.3
resolution: "eslint-plugin-storybook@npm:10.3.3"
dependencies:
"@typescript-eslint/utils": "npm:^8.8.1"
"@typescript-eslint/utils": "npm:^8.48.0"
peerDependencies:
eslint: ">=8"
storybook: ^10.1.4
checksum: 10c0/d68a0244318a386877de12d22ce309725f9f7057002f5182fa50fc965e26ced7d051432d7495046f13ee5803634ce53ef9e58cfe2866c3251f2ff94f8ab50e74
storybook: ^10.3.3
checksum: 10c0/501a07db230aefa5bb76882fe7b0a3e9a5db87fc29bbcc96b25e880a2ee97a81ff871cf364cb09e9ed9b67bc7d6cd0541755fd0ac778d3b68124289a4fdecde4
languageName: node
linkType: hard

Expand Down Expand Up @@ -27148,7 +27231,7 @@ __metadata:
languageName: node
linkType: hard

"ts-api-utils@npm:^2.4.0":
"ts-api-utils@npm:^2.4.0, ts-api-utils@npm:^2.5.0":
version: 2.5.0
resolution: "ts-api-utils@npm:2.5.0"
peerDependencies:
Expand Down
Loading