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',
- });
+ }
+ 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 },
+ );
}}
>
- }
- style={{ fontSize: '12px' }}
- >
- Share Session
-
-
+ 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,
+ }),
+ );
+ });
+});