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/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 + 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, + }), + ); + }); +});