From ab8af5ea4c2c3b17b959fbdf74b65b70dd172fdb Mon Sep 17 00:00:00 2001 From: Justice Le Date: Wed, 20 May 2026 22:46:31 +0700 Subject: [PATCH 1/2] fix(app): copy correct session URL on first Share Session click CopyToClipboard captured window.location.href into its text prop at render time, but nuqs flushes sid/sfrom/sto into the URL asynchronously (after startTransition + throttleMs). The panel rendered before the flush and froze the stale URL, so the first click copied a link without the session params. A reload "fixed" it only because the URL already contained the params on initial render. Replace CopyToClipboard with an onClick handler that reads window.location.href at click time (after nuqs has flushed) and uses the shared copyTextToClipboard util for the non-HTTPS textarea fallback and error reporting. Fixes #2313 --- packages/app/src/SessionSidePanel.tsx | 38 +++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/packages/app/src/SessionSidePanel.tsx b/packages/app/src/SessionSidePanel.tsx index 15a8ad907c..ddc26e3cca 100644 --- a/packages/app/src/SessionSidePanel.tsx +++ b/packages/app/src/SessionSidePanel.tsx @@ -1,5 +1,4 @@ import { useCallback, useMemo, useState } from 'react'; -import CopyToClipboard from 'react-copy-to-clipboard'; import { useHotkeys } from 'react-hotkeys-hook'; import { DateRange, @@ -17,6 +16,10 @@ import { getInitialDrawerWidthPercent, } from '@/components/DrawerUtils'; import useResizable from '@/hooks/useResizable'; +import { + CLIPBOARD_ERROR_MESSAGE, + copyTextToClipboard, +} from '@/utils/clipboard'; import { Session } from './sessions'; import SessionSubpanel from './SessionSubpanel'; @@ -125,24 +128,25 @@ export default function SessionSidePanel({ isFullWidth={isFullWidth} onToggle={toggleFullWidth} /> - { - notifications.show({ - color: 'green', - message: 'Copied link to clipboard', - }); + - + Share Session + From f9128b0ad8aefb6ddbb245f63ba7b0986a227599 Mon Sep 17 00:00:00 2001 From: Justice Le Date: Thu, 21 May 2026 21:17:38 +0700 Subject: [PATCH 2/2] test(app): cover Share Session click-time URL read + add changeset Address PR #2318 review comments: - Add an RTL test that mutates window.location after mount and asserts copyTextToClipboard receives the post-mutation URL, plus a failure-branch test for the red CLIPBOARD_ERROR_MESSAGE toast. - Add a changeset for @hyperdx/app. --- .changeset/share-session-stale-url.md | 7 ++ .../src/__tests__/SessionSidePanel.test.tsx | 118 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 .changeset/share-session-stale-url.md create mode 100644 packages/app/src/__tests__/SessionSidePanel.test.tsx diff --git a/.changeset/share-session-stale-url.md b/.changeset/share-session-stale-url.md new file mode 100644 index 0000000000..0f99d4b6f3 --- /dev/null +++ b/.changeset/share-session-stale-url.md @@ -0,0 +1,7 @@ +--- +'@hyperdx/app': patch +--- + +fix(app): copy correct session URL on first Share Session click + +The Share Session button captured `window.location.href` at render time, which ran before `nuqs` flushed `sid`/`sfrom`/`sto` into the URL. The button now reads the URL at click time via the shared `copyTextToClipboard` util, so the first copy always contains the session params (no reload needed). diff --git a/packages/app/src/__tests__/SessionSidePanel.test.tsx b/packages/app/src/__tests__/SessionSidePanel.test.tsx new file mode 100644 index 0000000000..e813942c96 --- /dev/null +++ b/packages/app/src/__tests__/SessionSidePanel.test.tsx @@ -0,0 +1,118 @@ +import { MantineProvider } from '@mantine/core'; +import { Notifications, notifications } from '@mantine/notifications'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import SessionSidePanel from '../SessionSidePanel'; +import { + CLIPBOARD_ERROR_MESSAGE, + copyTextToClipboard, +} from '../utils/clipboard'; + +jest.mock('../SessionSubpanel', () => ({ + __esModule: true, + default: () =>
, +})); + +jest.mock('../utils/clipboard', () => ({ + __esModule: true, + CLIPBOARD_ERROR_MESSAGE: + 'Could not access the clipboard. Check browser permissions or use HTTPS.', + copyTextToClipboard: jest.fn(), +})); + +jest.mock('@mantine/notifications', () => { + const actual = jest.requireActual('@mantine/notifications'); + return { + ...actual, + notifications: { + ...actual.notifications, + show: jest.fn(), + }, + }; +}); + +const mockedCopy = copyTextToClipboard as jest.MockedFunction< + typeof copyTextToClipboard +>; +const mockedShow = notifications.show as jest.MockedFunction< + typeof notifications.show +>; + +function setLocationHref(url: string) { + const parsed = new URL(url, 'http://localhost'); + window.history.replaceState(null, '', parsed.pathname + parsed.search); +} + +function renderPanel() { + return render( + + + + , + ); +} + +describe('SessionSidePanel - Share Session', () => { + beforeEach(() => { + mockedCopy.mockReset(); + mockedShow.mockReset(); + setLocationHref('/sessions?sessionSource=src&from=1&to=2'); + }); + + it('copies the URL as it exists at click time, not at render time', async () => { + mockedCopy.mockResolvedValue(true); + + renderPanel(); + + setLocationHref( + '/sessions?sessionSource=src&from=1&to=2&sid=abc&sfrom=10&sto=20', + ); + + fireEvent.click(screen.getByRole('button', { name: /share session/i })); + + await waitFor(() => expect(mockedCopy).toHaveBeenCalledTimes(1)); + expect(mockedCopy).toHaveBeenCalledWith( + 'http://localhost/sessions?sessionSource=src&from=1&to=2&sid=abc&sfrom=10&sto=20', + ); + + await waitFor(() => expect(mockedShow).toHaveBeenCalledTimes(1)); + expect(mockedShow).toHaveBeenCalledWith( + expect.objectContaining({ + color: 'green', + message: 'Copied link to clipboard', + }), + ); + }); + + it('shows an error notification when the clipboard copy fails', async () => { + mockedCopy.mockResolvedValue(false); + + renderPanel(); + + fireEvent.click(screen.getByRole('button', { name: /share session/i })); + + await waitFor(() => expect(mockedShow).toHaveBeenCalledTimes(1)); + expect(mockedShow).toHaveBeenCalledWith( + expect.objectContaining({ + color: 'red', + message: CLIPBOARD_ERROR_MESSAGE, + }), + ); + }); +});