diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionDeleteButton.tsx
new file mode 100644
index 000000000..7f1199157
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionDeleteButton.tsx
@@ -0,0 +1,72 @@
+'use client';
+import { useQueryClient } from '@tanstack/react-query';
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+
+const invalidationKeys = [
+ ['sessions:stats'],
+ ['websites:stats'],
+ ['websites:events'],
+ ['websites:events:stats'],
+ ['websites:revenue:chart'],
+ ['websites:revenue:metrics'],
+ ['realtime'],
+];
+
+export function SessionDeleteButton({
+ websiteId,
+ sessionId,
+ onSave,
+}: {
+ websiteId: string;
+ sessionId: string;
+ onSave?: () => void;
+}) {
+ const queryClient = useQueryClient();
+ const { t, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(
+ `/websites/${websiteId}/sessions/${sessionId}`,
+ );
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: async () => {
+ touch('sessions');
+ touch('replays');
+ touch('revenue-sessions');
+
+ await Promise.all(
+ invalidationKeys.map(queryKey => queryClient.invalidateQueries({ queryKey })),
+ );
+
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ }
+ title={t(labels.confirm)}
+ variant="quiet"
+ width="400px"
+ >
+ {({ close }) => (
+
+ )}
+
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.test.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.test.tsx
new file mode 100644
index 000000000..d07830146
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.test.tsx
@@ -0,0 +1,94 @@
+import { expect, test, vi } from 'vitest';
+import { render, screen } from '@/test/render';
+import { SessionProfile } from './SessionProfile';
+
+const mockUseConfig = vi.fn();
+const mockUseWebsiteSessionQuery = vi.fn();
+
+vi.mock('@/components/hooks', async () => {
+ const actual = await vi.importActual('@/components/hooks');
+
+ return {
+ ...actual,
+ useConfig: () => mockUseConfig(),
+ useMessages: () => ({
+ t: (value: string) => value,
+ labels: {
+ activity: 'Activity',
+ properties: 'Properties',
+ replays: 'Replays',
+ },
+ }),
+ useMobile: () => ({ isMobile: false }),
+ useWebsiteSessionQuery: () => mockUseWebsiteSessionQuery(),
+ };
+});
+
+vi.mock('@/components/common/Avatar', () => ({
+ Avatar: () => Avatar
,
+}));
+
+vi.mock('@/components/common/LoadingPanel', () => ({
+ LoadingPanel: ({ children, data }: { children: React.ReactNode; data: unknown }) =>
+ data ? {children}
: null,
+}));
+
+vi.mock('./SessionActivity', () => ({
+ SessionActivity: () => SessionActivity
,
+}));
+
+vi.mock('./SessionDeleteButton', () => ({
+ SessionDeleteButton: () => ,
+}));
+
+vi.mock('./SessionData', () => ({
+ SessionData: () => SessionData
,
+}));
+
+vi.mock('./SessionInfo', () => ({
+ SessionInfo: () => SessionInfo
,
+}));
+
+vi.mock('./SessionReplaysDataTable', () => ({
+ SessionReplaysDataTable: () => SessionReplaysDataTable
,
+}));
+
+vi.mock('./SessionStats', () => ({
+ SessionStats: () => SessionStats
,
+}));
+
+function createSessionQueryResult(canDelete: boolean) {
+ return {
+ data: {
+ id: 'session-1',
+ canDelete,
+ firstAt: new Date('2026-07-24T00:00:00.000Z'),
+ lastAt: new Date('2026-07-24T00:05:00.000Z'),
+ distinctId: 'distinct-1',
+ },
+ isLoading: false,
+ error: undefined,
+ };
+}
+
+test('hides the delete button on share routes', () => {
+ mockUseConfig.mockReturnValue({ sessionDeletionEnabled: true });
+ mockUseWebsiteSessionQuery.mockReturnValue(createSessionQueryResult(true));
+
+ render( null} />, {
+ route: '/share/slug/sessions?session=session-1',
+ });
+
+ expect(screen.queryByTestId('session-delete-button')).not.toBeInTheDocument();
+});
+
+test('shows the delete button on regular website routes when deletion is allowed', () => {
+ mockUseConfig.mockReturnValue({ sessionDeletionEnabled: true });
+ mockUseWebsiteSessionQuery.mockReturnValue(createSessionQueryResult(true));
+
+ render( null} />, {
+ route: '/websites/website-1/sessions?session=session-1',
+ });
+
+ expect(screen.getByTestId('session-delete-button')).toBeInTheDocument();
+});
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
index 1c0405472..a6ccbad72 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx
@@ -10,11 +10,18 @@ import {
Tabs,
TextField,
} from '@umami/react-zen';
-import { X } from 'lucide-react';
+import {
+ useConfig,
+ useMessages,
+ useMobile,
+ useNavigation,
+ useWebsiteSessionQuery,
+} from '@/components/hooks';
+import { X } from '@/components/icons';
import { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel';
-import { useMessages, useMobile, useWebsiteSessionQuery } from '@/components/hooks';
import { SessionActivity } from './SessionActivity';
+import { SessionDeleteButton } from './SessionDeleteButton';
import { SessionData } from './SessionData';
import { SessionInfo } from './SessionInfo';
import { SessionReplaysDataTable } from './SessionReplaysDataTable';
@@ -32,8 +39,14 @@ export function SessionProfile({
onClose?: () => void;
}) {
const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
+ const config = useConfig();
const { t, labels } = useMessages();
const { isMobile } = useMobile();
+ const { pathname } = useNavigation();
+ const isSharePage = pathname.includes('/share/');
+ const showDeleteButton = Boolean(
+ !isSharePage && config?.sessionDeletionEnabled && data?.canDelete,
+ );
return (
{onClose && (
-
+
+ {showDeleteButton && (
+
+ )}