Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .changeset/share-session-stale-url.md
Original file line number Diff line number Diff line change
@@ -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).
38 changes: 21 additions & 17 deletions packages/app/src/SessionSidePanel.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -125,24 +128,25 @@ export default function SessionSidePanel({
isFullWidth={isFullWidth}
onToggle={toggleFullWidth}
/>
<CopyToClipboard
text={window.location.href}
onCopy={() => {
notifications.show({
color: 'green',
message: 'Copied link to clipboard',
});
<Button
variant="secondary"
size="sm"
leftSection={<IconLink size={14} />}
style={{ fontSize: '12px' }}
onClick={async () => {
const ok = await copyTextToClipboard(window.location.href);
notifications.show(
ok
? {
color: 'green',
message: 'Copied link to clipboard',
}
: { color: 'red', message: CLIPBOARD_ERROR_MESSAGE },
);
}}
>
<Button
variant="secondary"
size="sm"
leftSection={<IconLink size={14} />}
style={{ fontSize: '12px' }}
>
Share Session
</Button>
</CopyToClipboard>
Share Session
</Button>
<ActionIcon variant="secondary" size="md" onClick={onClose}>
<IconX size={14} />
</ActionIcon>
Expand Down
118 changes: 118 additions & 0 deletions packages/app/src/__tests__/SessionSidePanel.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div data-testid="session-subpanel-mock" />,
}));

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(
<MantineProvider>
<Notifications />
<SessionSidePanel
traceSource={{ id: 'trace-source' } as any}
sessionSource={{ id: 'session-source' } as any}
sessionId="sid-abc"
session={
{
sessionId: 'sid-abc',
userEmail: 'user@example.com',
minTimestamp: '2024-01-01T00:00:00Z',
maxTimestamp: '2024-01-01T01:00:00Z',
errorCount: '0',
sessionCount: '5',
} as any
}
dateRange={[new Date(0), new Date(1)]}
onClose={jest.fn()}
/>
</MantineProvider>,
);
}

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