From 40568493d4314a415a0703f9e3d3bceeb150dbe2 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 30 Jan 2026 07:59:20 -0600 Subject: [PATCH 01/12] Next: Sessions Dashboard --- .../events/components/events-overview.svelte | 21 +- .../components/views/session-events.svelte | 139 ++++++ .../lib/features/organizations/api.svelte.ts | 5 + .../src/lib/features/sessions/api.svelte.ts | 121 ++++++ .../sessions-dashboard-chart.svelte | 91 ++++ .../sessions-stats-dashboard.svelte | 87 ++++ .../(app)/(components)/layouts/sidebar.svelte | 22 + .../ClientApp/src/routes/(app)/+layout.svelte | 7 +- .../src/routes/(app)/routes.svelte.ts | 7 + .../src/routes/(app)/sessions/+page.svelte | 394 ++++++++++++++++++ 10 files changed, 892 insertions(+), 2 deletions(-) create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-stats-dashboard.svelte create mode 100644 src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte index 2b407c2e42..69c02f1b04 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte @@ -22,6 +22,7 @@ import Overview from './views/overview.svelte'; import PromotedExtendedData from './views/promoted-extended-data.svelte'; import Request from './views/request.svelte'; + import SessionEvents from './views/session-events.svelte'; import TraceLog from './views/trace-log.svelte'; interface Props { @@ -32,12 +33,23 @@ let { filterChanged, handleError, id }: Props = $props(); + // Helper to get session ID from event + function getSessionId(event?: null | PersistentEvent): string | undefined { + if (!event) return undefined; + // For session start events, use reference_id + if (event.type === 'session') { + return event.reference_id ?? undefined; + } + // For other events, check @ref:session in data + return event.data?.['@ref:session'] as string | undefined; + } + function getTabs(event?: null | PersistentEvent, project?: ViewProject): TabType[] { if (!event) { return []; } - const tabs = ['Overview']; + const tabs: TabType[] = ['Overview']; if (hasErrorOrSimpleError(event)) { tabs.push('Exception'); } @@ -54,6 +66,11 @@ tabs.push('Trace Log'); } + // Add Session Events tab if event has a session reference + if (getSessionId(event)) { + tabs.push('Session Events'); + } + if (!project) { return tabs; } @@ -168,6 +185,8 @@ {:else if tab === 'Trace Log'} + {:else if tab === 'Session Events'} + {:else if tab === 'Extended Data'} {:else} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte new file mode 100644 index 0000000000..d683e7cdcf --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte @@ -0,0 +1,139 @@ + + +{#if !hasPremiumFeatures} + + + Premium Feature + + Sessions are a premium feature. Upgrade your plan to view session events. + + +{/if} + +
+ {#if isSessionStart} + + + + Occurred On + + + + + + + Duration + + + {#if isActiveSession} + + {/if} + + {#if event.data?.sessionend} + (ended ) + {/if} + + + + + {/if} + +

Session Events

+ + {#if sessionEventsQuery.isLoading} +
+ {#each Array.from({ length: 5 }, (_, i) => i) as i (i)} + + {/each} +
+ {:else if sessionEventsQuery.isError} + + Error loading session events + {sessionEventsQuery.error?.message ?? 'Unknown error'} + + {:else if sessionEventsQuery.data && sessionEventsQuery.data.length > 0} + + + + Summary + When + + + + {#each sessionEventsQuery.data as sessionEvent (sessionEvent.id)} + + + + {sessionEvent.id} + + + + + + + {/each} + + + {:else} +

No session events found.

+ {/if} +
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts index b3be2e34c2..4edfd67dee 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts @@ -359,6 +359,11 @@ export function postOrganization() { onSuccess: (organization: ViewOrganization) => { queryClient.setQueryData(queryKeys.id(organization.id, 'stats'), organization); queryClient.setQueryData(queryKeys.id(organization.id, undefined), organization); + // Invalidate organizations list so it includes the new org + queryClient.invalidateQueries({ queryKey: queryKeys.list(undefined) }); + queryClient.invalidateQueries({ queryKey: queryKeys.list('stats') }); + // Invalidate user query since organization_ids changed on the backend + queryClient.invalidateQueries({ queryKey: ['User', 'me'] }); } })); } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts new file mode 100644 index 0000000000..01391d7261 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts @@ -0,0 +1,121 @@ +import type { CountResult } from '$shared/models'; + +import { accessToken } from '$features/auth/index.svelte'; +import { DEFAULT_OFFSET } from '$shared/api/api.svelte'; +import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; +import { createQuery, useQueryClient } from '@tanstack/svelte-query'; + +import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index'; + +export const queryKeys = { + organizations: (id: string | undefined) => [...queryKeys.type, 'organizations', id] as const, + organizationsCount: (id: string | undefined, params?: GetOrganizationSessionsCountRequest['params']) => + [...queryKeys.organizations(id), 'count', params] as const, + projects: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const, + projectsCount: (id: string | undefined, params?: GetProjectSessionsCountRequest['params']) => [...queryKeys.projects(id), 'count', params] as const, + sessionEvents: (id: string | undefined) => [...queryKeys.type, 'session', id] as const, + type: ['Session'] as const +}; + +export interface GetSessionsParams { + after?: string; + before?: string; + filter?: string; + limit?: number; + mode?: 'summary'; + offset?: string; + page?: number; + sort?: string; + time?: string; +} + +export interface GetOrganizationSessionsCountRequest { + enabled?: () => boolean; + params?: { + aggregations?: string; + filter?: string; + offset?: string; + time?: string; + }; + route: { + organizationId: string | undefined; + }; +} + +export interface GetProjectSessionsCountRequest { + params?: { + aggregations?: string; + filter?: string; + offset?: string; + time?: string; + }; + route: { + projectId: string | undefined; + }; +} + +export interface GetSessionEventsRequest { + params?: { + after?: string; + before?: string; + filter?: string; + limit?: number; + mode?: 'summary'; + offset?: string; + sort?: string; + time?: string; + }; + route: { + sessionId: string | undefined; + }; +} + +/** + * Get session count with aggregations for stats and chart data. + * Uses aggregation: avg:value cardinality:user date:(date^offset cardinality:user) + */ +export function getOrganizationSessionsCountQuery(request: GetOrganizationSessionsCountRequest) { + const queryClient = useQueryClient(); + + return createQuery(() => ({ + enabled: () => !!accessToken.current && !!request.route.organizationId && (request.enabled?.() ?? true), + queryClient, + queryFn: async ({ signal }: { signal: AbortSignal }) => { + const client = useFetchClient(); + const response = await client.getJSON(`/organizations/${request.route.organizationId}/events/sessions/count`, { + params: { + ...(DEFAULT_OFFSET ? { offset: DEFAULT_OFFSET } : {}), + ...request.params + }, + signal + }); + + return response.data!; + }, + queryKey: queryKeys.organizationsCount(request.route.organizationId, request.params) + })); +} + +/** + * Get events within a session by session ID. + * Uses endpoint: /events/sessions/{sessionId} + */ +export function getSessionEventsQuery(request: GetSessionEventsRequest) { + return createQuery[], ProblemDetails>(() => ({ + enabled: () => !!accessToken.current && !!request.route.sessionId, + queryFn: async ({ signal }: { signal: AbortSignal }) => { + const client = useFetchClient(); + const response = await client.getJSON[]>(`events/sessions/${request.route.sessionId}`, { + params: { + ...(DEFAULT_OFFSET ? { offset: DEFAULT_OFFSET } : {}), + mode: 'summary', + ...request.params + }, + signal + }); + + return response.data!; + }, + queryKey: queryKeys.sessionEvents(request.route.sessionId) + })); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte new file mode 100644 index 0000000000..12053f9d18 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte @@ -0,0 +1,91 @@ + + +
+ {#if isLoading} + + {:else} + + d.sessions)))]} + {series} + axis={false} + grid={false} + brush={{ + onBrushEnd: (detail) => { + const [start, end] = detail.xDomain ?? []; + if (start instanceof Date && end instanceof Date) { + onRangeSelect?.(start, end); + } + } + }} + props={{ + area: { + curve: curveLinear + }, + canvas: { + class: 'cursor-crosshair' + }, + svg: { + class: 'cursor-crosshair' + } + }} + > + {#snippet tooltip()} + formatDateLabel(v as Date)} /> + {/snippet} + + + {/if} +
diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-stats-dashboard.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-stats-dashboard.svelte new file mode 100644 index 0000000000..4a831788f7 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-stats-dashboard.svelte @@ -0,0 +1,87 @@ + + +
+ + + Sessions + + + + {#if isLoading} + + {:else} +
+ +
+ {/if} +
+
+ + + + Sessions Per Hour + + + + {#if isLoading} + + {:else} +
+ {avgPerHour?.toFixed(1) ?? '0'} +
+ {/if} +
+
+ + + + Users + + + + {#if isLoading} + + {:else} +
+ +
+ {/if} +
+
+ + + + Average Duration + + + + {#if isLoading} + + {:else} +
+ + +
+ {/if} +
+
+
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar.svelte index e8a486d5f2..1bba323384 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/(components)/layouts/sidebar.svelte @@ -20,6 +20,7 @@ let { footer, header, routes, ...props }: Props = $props(); const dashboardRoutes = $derived(routes.filter((route) => route.group === 'Dashboards')); + const reportRoutes = $derived(routes.filter((route) => route.group === 'Reports')); const settingsRoutes = $derived(routes.filter((route) => route.group === 'Settings')); const settingsIsActive = $derived(settingsRoutes.some((route) => route.href === page.url.pathname)); @@ -62,6 +63,27 @@ + {#if reportRoutes.length > 0} + + Reports + + {#each reportRoutes as route (route.href)} + {@const Icon = route.icon} + + + {#snippet child({ props })} + + + {route.title} + + {/snippet} + + + {/each} + + + {/if} + diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte index eb7e0da037..46cecdd3e0 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte @@ -170,7 +170,12 @@ const organizations = $derived(organizationsQuery.data?.data ?? []); const impersonatingOrganizationId = $derived.by(() => { - const isUserOrganization = meQuery.data?.organization_ids.includes(organization.current ?? ''); + // Only consider impersonation if user data is loaded and user has organizations + const userOrgIds = meQuery.data?.organization_ids; + if (!userOrgIds || userOrgIds.length === 0 || !organization.current) { + return undefined; + } + const isUserOrganization = userOrgIds.includes(organization.current); return isUserOrganization ? undefined : organization.current; }); diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts b/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts index 30bb166562..14e61b85b8 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/routes.svelte.ts @@ -6,6 +6,7 @@ import EventStream from '@lucide/svelte/icons/calendar-arrow-down'; import Events from '@lucide/svelte/icons/calendar-days'; import Support from '@lucide/svelte/icons/circle-help'; import GitHub from '@lucide/svelte/icons/github'; +import Sessions from '@lucide/svelte/icons/timer'; import type { NavigationItem } from '../routes.svelte'; @@ -35,6 +36,12 @@ export function routes(): NavigationItem[] { icon: EventStream, title: 'Event Stream' }, + { + group: 'Reports', + href: resolve('/(app)/sessions'), + icon: Sessions, + title: 'Sessions' + }, { group: 'Help', href: 'https://exceptionless.com/docs/', diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte new file mode 100644 index 0000000000..2439ae4b2e --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte @@ -0,0 +1,394 @@ + + +
+ {#if organizationQuery.isSuccess && !hasPremiumFeatures} + + + Premium Feature + + Upgrade now to enable sessions and other premium features! + + + {/if} + +
+

Sessions

+
+ + + +
+
+
+ + +
+ + +
+
+ +
+ + + + + + {#snippet footerChildren()} + + +
+ + +
+ {/snippet} +
+
+
+ + (selectedEventId = null)} open={!!selectedEventId}> + + + Event Details + +
+ (selectedEventId = null)} /> +
+
+
From eb6c85aaf9fd1b98052c9f04e38ab4c0fad25ae1 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 2 Feb 2026 21:15:38 -0600 Subject: [PATCH 02/12] feat: add update scripts for shadcn and skills in package.json --- src/Exceptionless.Web/ClientApp/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Exceptionless.Web/ClientApp/package.json b/src/Exceptionless.Web/ClientApp/package.json index 1d2d918657..2ff08d68bf 100644 --- a/src/Exceptionless.Web/ClientApp/package.json +++ b/src/Exceptionless.Web/ClientApp/package.json @@ -22,7 +22,9 @@ "test": "npm run test:unit -- --run && npm run test:e2e", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "upgrade": "ncu -i" + "update": "ncu -i", + "update:shadcn": "npx shadcn-svelte@latest update", + "update:skills": "npx skills update" }, "devDependencies": { "@chromatic-com/storybook": "^5.0.1", From 44c9500606e264559e060b9a485f90d12c38c19d Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 2 Feb 2026 21:19:31 -0600 Subject: [PATCH 03/12] style: update component styles for improved dark mode support and consistency --- .../src/lib/features/sessions/api.svelte.ts | 2 +- .../ui/calendar/calendar-month-select.svelte | 6 +++++- .../ui/calendar/calendar-year-select.svelte | 6 +++++- .../components/ui/field/field-error.svelte | 2 +- .../shared/components/ui/pagination/index.ts | 16 ++++++++-------- .../components/ui/select/select-group.svelte | 2 +- .../components/ui/separator/separator.svelte | 2 +- .../components/ui/tooltip/tooltip-content.svelte | 2 +- .../ClientApp/src/lib/hooks/is-mobile.svelte.ts | 6 +++--- 9 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts index 01391d7261..2fb17b3338 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts @@ -82,7 +82,7 @@ export function getOrganizationSessionsCountQuery(request: GetOrganizationSessio queryClient, queryFn: async ({ signal }: { signal: AbortSignal }) => { const client = useFetchClient(); - const response = await client.getJSON(`/organizations/${request.route.organizationId}/events/sessions/count`, { + const response = await client.getJSON(`/organizations/${request.route.organizationId}/events/count`, { params: { ...(DEFAULT_OFFSET ? { offset: DEFAULT_OFFSET } : {}), ...request.params diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/calendar/calendar-month-select.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/calendar/calendar-month-select.svelte index 8d88deb552..664afab8d6 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/calendar/calendar-month-select.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/calendar/calendar-month-select.svelte @@ -18,7 +18,11 @@ className )} > - + {#snippet child({ props, monthItems, selectedMonthItem })} {#each yearItems as yearItem (yearItem.value)} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/field/field-error.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/field/field-error.svelte index 1d5cc5f595..94a46c2bf8 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/field/field-error.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/field/field-error.svelte @@ -19,7 +19,7 @@ if (children) return true; // no errors - if (!errors) return false; + if (!errors || errors.length === 0) return false; // has an error but no message if (errors.length === 1 && !errors[0]?.message) { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/pagination/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/pagination/index.ts index dbe47abfcd..d4b0b642a9 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/pagination/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/pagination/index.ts @@ -16,10 +16,10 @@ export { Item, Link, PrevButton, //old - NextButton, //old - Ellipsis, - Previous, - Next, + NextButton, //old + Ellipsis, + Previous, + Next, // Root as Pagination, Content as PaginationContent, @@ -27,8 +27,8 @@ export { Item as PaginationItem, Link as PaginationLink, PrevButton as PaginationPrevButton, //old - NextButton as PaginationNextButton, //old - Ellipsis as PaginationEllipsis, - Previous as PaginationPrevious, - Next as PaginationNext + NextButton as PaginationNextButton, //old + Ellipsis as PaginationEllipsis, + Previous as PaginationPrevious, + Next as PaginationNext, }; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/select/select-group.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/select/select-group.svelte index 5454fdb395..a1f43bf3d0 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/select/select-group.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/select/select-group.svelte @@ -4,4 +4,4 @@ let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props(); - + diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/separator/separator.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/separator/separator.svelte index e11a6f5bab..f40999fa71 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/separator/separator.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/separator/separator.svelte @@ -14,7 +14,7 @@ bind:ref data-slot={dataSlot} class={cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", + "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px", className )} {...restProps} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/tooltip/tooltip-content.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/tooltip/tooltip-content.svelte index 788ec34db9..2662522874 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/tooltip/tooltip-content.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/ui/tooltip/tooltip-content.svelte @@ -37,7 +37,7 @@ {#snippet child({ props })}
Date: Mon, 9 Feb 2026 07:53:29 -0600 Subject: [PATCH 04/12] pr feedback --- .../components/views/session-events.svelte | 6 ++-- .../src/lib/features/sessions/api.svelte.ts | 27 +++++++++-------- .../src/lib/hooks/is-mobile.svelte.ts | 6 ++-- .../src/routes/(app)/sessions/+page.svelte | 29 ++++++++++++++++++- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte index d683e7cdcf..bfc6954f6b 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte @@ -63,9 +63,7 @@ Premium Feature - - Sessions are a premium feature. Upgrade your plan to view session events. - + Sessions are a premium feature. Upgrade your plan to view session events. {/if} @@ -120,7 +118,7 @@ {#each sessionEventsQuery.data as sessionEvent (sessionEvent.id)} - + {sessionEvent.id} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts index 2fb17b3338..1116140d3b 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/api.svelte.ts @@ -1,3 +1,4 @@ +import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index'; import type { CountResult } from '$shared/models'; import { accessToken } from '$features/auth/index.svelte'; @@ -5,8 +6,6 @@ import { DEFAULT_OFFSET } from '$shared/api/api.svelte'; import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient'; import { createQuery, useQueryClient } from '@tanstack/svelte-query'; -import type { EventSummaryModel, SummaryTemplateKeys } from '$features/events/components/summary/index'; - export const queryKeys = { organizations: (id: string | undefined) => [...queryKeys.type, 'organizations', id] as const, organizationsCount: (id: string | undefined, params?: GetOrganizationSessionsCountRequest['params']) => @@ -17,18 +16,6 @@ export const queryKeys = { type: ['Session'] as const }; -export interface GetSessionsParams { - after?: string; - before?: string; - filter?: string; - limit?: number; - mode?: 'summary'; - offset?: string; - page?: number; - sort?: string; - time?: string; -} - export interface GetOrganizationSessionsCountRequest { enabled?: () => boolean; params?: { @@ -70,6 +57,18 @@ export interface GetSessionEventsRequest { }; } +export interface GetSessionsParams { + after?: string; + before?: string; + filter?: string; + limit?: number; + mode?: 'summary'; + offset?: string; + page?: number; + sort?: string; + time?: string; +} + /** * Get session count with aggregations for stats and chart data. * Uses aggregation: avg:value cardinality:user date:(date^offset cardinality:user) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/hooks/is-mobile.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/hooks/is-mobile.svelte.ts index acbe8ef575..2dfb0eb804 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/hooks/is-mobile.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/hooks/is-mobile.svelte.ts @@ -3,7 +3,7 @@ import { MediaQuery } from 'svelte/reactivity'; const DEFAULT_MOBILE_BREAKPOINT = 768; export class IsMobile extends MediaQuery { - constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { - super(`max-width: ${breakpoint - 1}px`); - } + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } } diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte index 2439ae4b2e..4451dc2980 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/sessions/+page.svelte @@ -31,7 +31,7 @@ import { getColumns } from '$features/events/components/table/options.svelte'; import { getOrganizationQuery } from '$features/organizations/api.svelte'; import { organization } from '$features/organizations/context.svelte'; - import { getOrganizationSessionsCountQuery } from '$features/sessions/api.svelte'; + import { getOrganizationSessionsCountQuery, queryKeys } from '$features/sessions/api.svelte'; import SessionsDashboardChart from '$features/sessions/components/sessions-dashboard-chart.svelte'; import SessionsStatsDashboard from '$features/sessions/components/sessions-stats-dashboard.svelte'; import * as agg from '$features/shared/api/aggregations'; @@ -43,11 +43,14 @@ import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient'; import ExternalLink from '@lucide/svelte/icons/external-link'; import InfoIcon from '@lucide/svelte/icons/info'; + import { useQueryClient } from '@tanstack/svelte-query'; import { createTable } from '@tanstack/svelte-table'; import { queryParamsState } from 'kit-query-params'; import { useEventListener, watch } from 'runed'; import { throttle } from 'throttle-debounce'; + const queryClient = useQueryClient(); + let selectedEventId: null | string = $state(null); function rowclick(row: EventSummaryModel) { selectedEventId = row.id; @@ -202,6 +205,15 @@ } await loadData(); + + // Invalidate and refetch the stats query to ensure aggregations are updated + await queryClient.invalidateQueries({ + queryKey: queryKeys.organizationsCount(organization.current, { + aggregations: `avg:value cardinality:user date:(date${DEFAULT_OFFSET ? `^${DEFAULT_OFFSET}` : ''} cardinality:user)`, + filter: activeFilter(), + time: eventsQueryParameters.time + }) + }); } async function loadData() { @@ -235,6 +247,21 @@ loadData(); }); + // Refetch stats when time or filter changes + $effect(() => { + const time = eventsQueryParameters.time; + const filter = eventsQueryParameters.filter; + + // Invalidate the stats query to trigger a refetch with new params + queryClient.invalidateQueries({ + queryKey: queryKeys.organizationsCount(organization.current, { + aggregations: `avg:value cardinality:user date:(date${DEFAULT_OFFSET ? `^${DEFAULT_OFFSET}` : ''} cardinality:user)`, + filter: activeFilter(), + time + }) + }); + }); + // Session stats query with aggregations const statsQuery = getOrganizationSessionsCountQuery({ params: { From 186d995be9506d62de5018d6bb176813b6603611 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 9 Feb 2026 21:17:26 -0600 Subject: [PATCH 05/12] PR feedback --- .../events/components/events-overview.svelte | 13 +---- .../events/components/log-level.svelte | 4 +- .../components/views/session-events.svelte | 21 ++----- .../lib/features/events/models/event-data.ts | 33 ----------- .../src/lib/features/events/utils/index.ts | 56 +++++++++++++++++++ .../lib/features/organizations/api.svelte.ts | 5 +- .../components/project-log-level.svelte | 4 +- .../sessions-stats-dashboard.svelte | 8 +-- .../faceted-filter-multi-select.svelte | 4 +- .../ClientApp/src/routes/(app)/+layout.svelte | 1 + .../src/routes/(app)/sessions/+page.svelte | 35 ++---------- 11 files changed, 82 insertions(+), 102 deletions(-) create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte index 69c02f1b04..c89dcd0823 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte @@ -16,6 +16,7 @@ import type { PersistentEvent } from '../models/index'; + import { getSessionId } from "../utils"; import Environment from './views/environment.svelte'; import Error from './views/error.svelte'; import ExtendedData from './views/extended-data.svelte'; @@ -33,17 +34,6 @@ let { filterChanged, handleError, id }: Props = $props(); - // Helper to get session ID from event - function getSessionId(event?: null | PersistentEvent): string | undefined { - if (!event) return undefined; - // For session start events, use reference_id - if (event.type === 'session') { - return event.reference_id ?? undefined; - } - // For other events, check @ref:session in data - return event.data?.['@ref:session'] as string | undefined; - } - function getTabs(event?: null | PersistentEvent, project?: ViewProject): TabType[] { if (!event) { return []; @@ -66,7 +56,6 @@ tabs.push('Trace Log'); } - // Add Session Events tab if event has a session reference if (getSessionId(event)) { tabs.push('Session Events'); } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte index 921967df9a..ca6973f3fa 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte @@ -1,6 +1,8 @@ {name} is attempting to use a premium feature. Upgrade now - to enable search and other premium features! + to enable {premiumFeatureName} and other premium features! diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-notifications.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-notifications.svelte index 36a2208b26..4ff0af6085 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-notifications.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/organization-notifications.svelte @@ -22,6 +22,7 @@ ignoreFree?: boolean; isChatEnabled: boolean; openChat: () => void; + premiumFeatureName?: string; requiresPremium?: boolean; } From e6c413b44673f24451b5f6289f586420523bda78 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 10 Feb 2026 21:41:24 -0600 Subject: [PATCH 07/12] More sessions work --- .../events/components/events-overview.svelte | 2 +- .../events/components/log-level.svelte | 2 +- .../events/components/views/overview.svelte | 17 +--- .../components/views/session-events.svelte | 96 ++++++++++--------- .../src/lib/features/events/utils.ts | 20 ++++ .../src/lib/features/events/utils/index.ts | 2 + .../premium-upgrade-notification.svelte | 2 +- .../hooks/use-premium-feature.svelte.ts | 33 +++++++ .../components/project-log-level.svelte | 2 +- 9 files changed, 112 insertions(+), 64 deletions(-) create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts create mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte index c89dcd0823..c5151a01cf 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/events-overview.svelte @@ -16,7 +16,7 @@ import type { PersistentEvent } from '../models/index'; - import { getSessionId } from "../utils"; + import { getSessionId } from '../utils'; import Environment from './views/environment.svelte'; import Error from './views/error.svelte'; import ExtendedData from './views/extended-data.svelte'; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte index ca6973f3fa..d6678e589a 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/log-level.svelte @@ -2,7 +2,7 @@ import { Badge } from '$comp/ui/badge'; import { type LogLevel } from '$features/events/models/event-data'; - import { getLogLevel } from "../utils"; + import { getLogLevel } from '../utils'; interface Props { level?: LogLevel; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte index 626daca8a4..e71e0f0a44 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/overview.svelte @@ -18,6 +18,7 @@ getStackTrace, hasErrorOrSimpleError } from '$features/events/persistent-event'; + import { getSessionStartDuration } from '$features/events/utils'; import ExternalLink from '@lucide/svelte/icons/external-link'; import Filter from '@lucide/svelte/icons/filter'; import Email from '@lucide/svelte/icons/mail'; @@ -66,22 +67,6 @@ let requestUrl = $derived(getRequestInfoUrl(event)); let requestUrlPath = $derived(getRequestInfoPath(event)); let version = $derived(event.data?.['@version']); - - function getSessionStartDuration(event: PersistentEvent): Date | number | string | undefined { - if (event.data?.sessionend) { - if (event.value) { - return event.value * 1000; - } - - if (event.date) { - return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); - } - - throw new Error('Completed session start event has no value or date'); - } - - return event.date; - } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte index bd0228bbeb..55f7916bff 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/views/session-events.svelte @@ -4,49 +4,47 @@ import { resolve } from '$app/paths'; import Duration from '$comp/formatters/duration.svelte'; import TimeAgo from '$comp/formatters/time-ago.svelte'; + import Live from '$comp/live.svelte'; import { A } from '$comp/typography'; import * as Alert from '$comp/ui/alert'; import { Skeleton } from '$comp/ui/skeleton'; import * as Table from '$comp/ui/table'; - import { getSessionId } from "$features/events/utils"; + import { getSessionStartDuration } from '$features/events/utils'; + import { getSessionId } from '$features/events/utils/index'; + import { usePremiumFeature } from '$features/organizations/hooks/use-premium-feature.svelte'; import { getSessionEventsQuery } from '$features/sessions/api.svelte'; import InfoIcon from '@lucide/svelte/icons/info'; interface Props { event: PersistentEvent; hasPremiumFeatures?: boolean; + time?: string; } - let { event, hasPremiumFeatures = true }: Props = $props(); + let { event, hasPremiumFeatures = true, time }: Props = $props(); const sessionId = $derived(getSessionId(event)); const isSessionStart = $derived(event.type === 'session'); - // Calculate session duration for session start events - const sessionDuration = $derived(() => { - if (!isSessionStart) return 0; - const sessionEnd = event.data?.sessionend as string | undefined; - if (sessionEnd) { - // Duration is stored in value field (in seconds) - return (event.data?.Value as number) ?? 0; - } - // Active session - calculate from now - const startDate = new Date(event.date); - return Math.floor((Date.now() - startDate.getTime()) / 1000); - }); + const userInfo = $derived(event.data?.['@user']); + const userIdentity = $derived(userInfo?.identity); + const userName = $derived(userInfo?.name); - const isActiveSession = $derived(isSessionStart && !event.data?.sessionend); - const sessionEventsQuery = getSessionEventsQuery({ - params: { - filter: '-type:heartbeat', - limit: 10 - }, - route: { - get sessionId() { - // Only provide sessionId if user has access to sessions feature - return hasPremiumFeatures ? sessionId : undefined; + const sessionEventsQuery = $derived(() => + getSessionEventsQuery({ + params: { + filter: '-type:heartbeat', + limit: 10, + ...(time ? { time } : {}) + }, + route: { + sessionId: hasPremiumFeatures ? sessionId : undefined } - } + }) + ); + + $effect(() => { + if (!hasPremiumFeatures) usePremiumFeature('Sessions'); }); @@ -62,53 +60,63 @@ {#if isSessionStart} - - Occurred On - - - - - - + Duration - - {#if isActiveSession} - + + + + {#if event.data?.sessionend} + (ended ) {/if} - + + {#if event.data?.sessionend} - (ended ) + (ended ) {/if} + {#if userIdentity} + + User Identity + + {userIdentity} + + {/if} + {#if userName} + + User Name + + {userName} + + {/if} {/if}

Session Events

- {#if sessionEventsQuery.isLoading} + {#if sessionEventsQuery().isPending}
{#each Array.from({ length: 5 }, (_, i) => i) as i (i)} {/each}
- {:else if sessionEventsQuery.isError} + {:else if sessionEventsQuery().isError} Error loading session events - {sessionEventsQuery.error?.message ?? 'Unknown error'} + {sessionEventsQuery().error?.message ?? 'Unknown error'} - {:else if sessionEventsQuery.data && sessionEventsQuery.data.length > 0} + {:else if (sessionEventsQuery().data ?? []).length > 0} Summary - When + Session Time - {#each sessionEventsQuery.data as sessionEvent (sessionEvent.id)} + {#each sessionEventsQuery().data ?? [] as sessionEvent (sessionEvent.id)} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts new file mode 100644 index 0000000000..9cfdfaaee4 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts @@ -0,0 +1,20 @@ +import type { PersistentEvent } from '$features/events/models'; + +/** + * Returns the session duration in milliseconds for a given event. + * If the session has ended, uses the event value or the difference between sessionend and start. + * If the session is active, returns the duration from start to now. + */ +export function getSessionStartDuration(event: PersistentEvent): number { + if (event.data?.sessionend) { + if (event.value) { + return event.value * 1000; + } + if (event.date) { + return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); + } + return 0; + } + // If session is active, duration is from start to now + return Date.now() - new Date(event.date).getTime(); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts index 09e07182b6..d32f24a6ad 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts @@ -1,5 +1,7 @@ +export { getSessionStartDuration } from '../utils'; import type { PersistentEvent } from '../models'; import type { LogLevel } from '../models/event-data'; + import { logLevels } from '../options'; export function getLogLevel(level?: LogLevel | null): LogLevel | null { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/notifications/premium-upgrade-notification.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/notifications/premium-upgrade-notification.svelte index 1a0fec103f..46976f1a4e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/notifications/premium-upgrade-notification.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/notifications/premium-upgrade-notification.svelte @@ -11,7 +11,7 @@ premiumFeatureName?: string; } - let { name, organizationId, premiumFeatureName = "search", ...restProps }: Props = $props(); + let { name, organizationId, premiumFeatureName = 'search', ...restProps }: Props = $props(); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts new file mode 100644 index 0000000000..0b98b0af57 --- /dev/null +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts @@ -0,0 +1,33 @@ +import { getContext, onDestroy } from 'svelte'; + +const NOTIFICATION_CONTEXT_KEY = 'organizationNotifications'; + +/** + * Triggers a premium feature notification for the current organization. + * @param featureName Feature name to display in the notification. + */ +export function usePremiumFeature(featureName?: string) { + const notifications = getContext(NOTIFICATION_CONTEXT_KEY) as undefined | { update: (fn: (n: any[]) => any[]) => void }; + let notificationId: null | number = null; + + if (notifications && featureName) { + const id = Date.now() + Math.floor(Math.random() * 10000); + notificationId = id; + notifications.update((n: any[]) => [ + ...n, + { + feature: featureName, + id, + message: `The feature "${featureName}" is available on premium plans.`, + timestamp: Date.now(), + type: 'premium-feature' + } + ]); + } + + onDestroy(() => { + if (!notifications || notificationId == null) return; + notifications.update((n: any[]) => n.filter((notif: any) => notif.id !== notificationId)); + notificationId = null; + }); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte index c14e83c0d8..072d4bc265 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte @@ -4,7 +4,7 @@ import * as DropdownMenu from '$comp/ui/dropdown-menu'; import { Skeleton } from '$comp/ui/skeleton'; import { logLevels } from '$features/events/options'; - import { getLogLevel, getLogLevelDisplayName } from "$features/events/utils"; + import { getLogLevel, getLogLevelDisplayName } from '$features/events/utils'; import { deleteProjectConfig, getProjectConfig, postProjectConfig } from '$features/projects/api.svelte'; import { Button } from '$features/shared/components/ui/button'; import ChevronDown from '@lucide/svelte/icons/chevron-down'; From c95d7390d551a713c37e27858774d6ce5021e687 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 11 Feb 2026 21:54:50 -0600 Subject: [PATCH 08/12] Fixed build error --- .../src/lib/features/events/utils.ts | 20 ------------------- .../src/lib/features/events/utils/index.ts | 20 ++++++++++++++++++- 2 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts deleted file mode 100644 index 9cfdfaaee4..0000000000 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PersistentEvent } from '$features/events/models'; - -/** - * Returns the session duration in milliseconds for a given event. - * If the session has ended, uses the event value or the difference between sessionend and start. - * If the session is active, returns the duration from start to now. - */ -export function getSessionStartDuration(event: PersistentEvent): number { - if (event.data?.sessionend) { - if (event.value) { - return event.value * 1000; - } - if (event.date) { - return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); - } - return 0; - } - // If session is active, duration is from start to now - return Date.now() - new Date(event.date).getTime(); -} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts index d32f24a6ad..d112a061c3 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts @@ -1,9 +1,27 @@ -export { getSessionStartDuration } from '../utils'; import type { PersistentEvent } from '../models'; import type { LogLevel } from '../models/event-data'; import { logLevels } from '../options'; +/** + * Returns the session duration in milliseconds for a given event. + * If the session has ended, uses the event value or the difference between sessionend and start. + * If the session is active, returns the duration from start to now. + */ +export function getSessionStartDuration(event: PersistentEvent): number { + if (event.data?.sessionend) { + if (event.value) { + return event.value * 1000; + } + if (event.date) { + return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); + } + return 0; + } + // If session is active, duration is from start to now + return Date.now() - new Date(event.date).getTime(); +} + export function getLogLevel(level?: LogLevel | null): LogLevel | null { switch (level?.toLowerCase().trim()) { case '0': From c436614c1930eed5856d2233c6334d90e9c70fd6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Thu, 12 Feb 2026 21:32:01 -0600 Subject: [PATCH 09/12] Updates date-time extensions and UI tweaks. Updates the Exceptionless.DateTimeExtensions package to version 5.0.0. Adds missing words to cSpell configuration. Improves date-time parsing by enforcing lowercase units, aligning with backend validation. Adjusts UI element dimensions for better responsiveness. --- .vscode/settings.json | 2 + .../components/project-log-level.svelte | 2 +- .../sessions-dashboard-chart.svelte | 2 +- .../features/shared/utils/datemath.test.ts | 56 +++++++++++++++++++ .../src/lib/features/shared/utils/datemath.ts | 4 +- .../Exceptionless.Web.csproj | 3 + 6 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3a3040926c..65e65f651f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "cSpell.words": [ "acacode", "autodocs", + "axllent", "azurestorage", "Backdoors", "Bisectable", @@ -45,6 +46,7 @@ "legos", "lucene", "lucide", + "mailpit", "mypass", "nameof", "navigatetofirstpage", diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte index 072d4bc265..facb79ab1e 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/project-log-level.svelte @@ -171,5 +171,5 @@ {:else} - + {/if} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte index 12053f9d18..4d5ec4cb8b 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/sessions/components/sessions-dashboard-chart.svelte @@ -83,7 +83,7 @@ }} > {#snippet tooltip()} - formatDateLabel(v as Date)} /> + formatDateLabel(v as Date)} /> {/snippet} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.test.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.test.ts index 44d5e80c1c..15f442793a 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.test.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.test.ts @@ -506,4 +506,60 @@ describe('DateMath Library', () => { }); }); }); + + describe('Elastic date-math unit enforcement (lowercase d)', () => { + it('should accept now-7d as a valid expression', () => { + const result = parseDateMath('now-7d'); + expect(result.success).toBe(true); + expect(result.date).toEqual(new Date('2025-09-13T14:30:00Z')); + }); + + it('should reject now-7D (uppercase D is not a valid Elastic unit)', () => { + const result = parseDateMath('now-7D'); + expect(result.success).toBe(false); + }); + + it('should accept now-1d and resolve correctly', () => { + const result = parseDateMath('now-1d'); + expect(result.success).toBe(true); + expect(result.date).toEqual(new Date('2025-09-19T14:30:00Z')); + }); + + it('should reject now-1D (uppercase D)', () => { + const result = parseDateMath('now-1D'); + expect(result.success).toBe(false); + }); + + it('should parse [now-7d TO now] range correctly', () => { + const range = parseDateMathRange('[now-7d TO now]'); + expect(range.start).toEqual(new Date('2025-09-13T14:30:00Z')); + expect(range.end).toEqual(new Date('2025-09-20T14:30:00Z')); + }); + + it('should fail to parse [now-7D TO now] range (uppercase D)', () => { + const range = parseDateMathRange('[now-7D TO now]'); + // Should fall back to default range since uppercase D is invalid + expect(range.start).toEqual(new Date('2012-02-01')); + expect(range.end.getTime()).toBeCloseTo(new Date().getTime(), -3); + }); + + it('should accept rounding with lowercase d: now/d', () => { + const result = parseDateMath('now/d'); + expect(result.success).toBe(true); + }); + + it('should accept now-1d/d (subtraction + rounding with lowercase d)', () => { + const result = parseDateMath('now-1d/d'); + expect(result.success).toBe(true); + expect(result.date).toEqual(new Date('2025-09-19T00:00:00Z')); + }); + + it('should only accept documented Elastic units: y, M, w, d, h, H, m, s', () => { + const validUnits = ['y', 'M', 'w', 'd', 'h', 'H', 'm', 's']; + validUnits.forEach((unit) => { + const result = parseDateMath(`now-1${unit}`); + expect(result.success).toBe(true); + }); + }); + }); }); diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.ts index 35fc4b64be..001e7d272f 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/shared/utils/datemath.ts @@ -34,10 +34,10 @@ export const TIME_UNIT_NAMES: Record = { * Enhanced to match all backend test cases exactly */ const DATE_MATH_REGEX = - /^(?now|\*|(?\d{4}-?\d{2}-?\d{2}(?:[T\s](?:\d{1,2}(?::?\d{2}(?::?\d{2})?)?(?:\.\d{1,3})?)?(?:[+-]\d{2}:?\d{2}|Z)?)?)(?:\|\|)?)(?(?:[+\-/]\d*[yMwdhHms])*)$/i; + /^(?now|\*|(?\d{4}-?\d{2}-?\d{2}(?:[T\s](?:\d{1,2}(?::?\d{2}(?::?\d{2})?)?(?:\.\d{1,3})?)?(?:[+-]\d{2}:?\d{2}|Z)?)?)(?:\|\|)?)(?(?:[+\-/]\d*[yMwdhHms])*)$/; /** Pre-compiled regex for operation parsing - more strict validation */ -const OPERATION_REGEX = /([+\-/])(\d*)([yMwdhHms])/gi; +const OPERATION_REGEX = /([+\-/])(\d*)([yMwdhHms])/g; /** Result of parsing a date math expression */ export interface DateMathResult { diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 68da397f09..3b3e0ba516 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -21,6 +21,9 @@ + + + From 1e14bce3eb1f34058f75ab1946a1e719447f74bc Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Sat, 14 Feb 2026 12:17:27 -0600 Subject: [PATCH 10/12] Fixed merge issue --- src/Exceptionless.Web/Exceptionless.Web.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 3b3e0ba516..4c8927bffa 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -23,7 +23,6 @@ - From aa9c084dc2243dbe42c92c2d9d5ec05c11ddee5c Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Sat, 14 Feb 2026 13:24:52 -0600 Subject: [PATCH 11/12] Improves UI and fixes minor issues Refactors UI elements to use consistent width classes. Updates skeleton heights for a more uniform look during loading states. Removes unused import. Moves session duration function to a more appropriate location. --- .../lib/features/events/models/event-data.ts | 2 - .../src/lib/features/events/utils/index.ts | 38 +++++++++---------- .../dialogs/leave-organization-dialog.svelte | 2 +- .../dialogs/remove-organization-dialog.svelte | 2 +- .../hooks/use-premium-feature.svelte.ts | 19 ++++++++-- .../remove-project-config-dialog.svelte | 2 +- .../dialogs/remove-project-dialog.svelte | 2 +- .../dialogs/reset-project-data-dialog.svelte | 4 +- .../components/table/options.svelte.ts | 2 +- .../tokens/components/table/options.svelte.ts | 4 +- .../dialogs/remove-user-dialog.svelte | 2 +- .../users/components/table/options.svelte.ts | 6 +-- .../dialogs/remove-webhook-dialog.svelte | 2 +- .../components/table/options.svelte.ts | 4 +- .../(components)/theme-preview.svelte | 12 +++--- .../[organizationId]/usage/+page.svelte | 2 +- .../project/[projectId]/usage/+page.svelte | 4 +- 17 files changed, 59 insertions(+), 50 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/event-data.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/event-data.ts index 61199eed10..1da46d594b 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/event-data.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/models/event-data.ts @@ -1,5 +1,3 @@ -import { logLevels } from '../options'; - export interface EnvironmentInfo { architecture?: string; available_physical_memory?: number; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts index d112a061c3..ed1ccf5494 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/utils/index.ts @@ -3,25 +3,6 @@ import type { LogLevel } from '../models/event-data'; import { logLevels } from '../options'; -/** - * Returns the session duration in milliseconds for a given event. - * If the session has ended, uses the event value or the difference between sessionend and start. - * If the session is active, returns the duration from start to now. - */ -export function getSessionStartDuration(event: PersistentEvent): number { - if (event.data?.sessionend) { - if (event.value) { - return event.value * 1000; - } - if (event.date) { - return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); - } - return 0; - } - // If session is active, duration is from start to now - return Date.now() - new Date(event.date).getTime(); -} - export function getLogLevel(level?: LogLevel | null): LogLevel | null { switch (level?.toLowerCase().trim()) { case '0': @@ -74,3 +55,22 @@ export function getSessionId(event?: null | PersistentEvent): string | undefined // For other events, check @ref:session in data return event.data?.['@ref:session'] as string | undefined; } + +/** + * Returns the session duration in milliseconds for a given event. + * If the session has ended, uses the event value or the difference between sessionend and start. + * If the session is active, returns the duration from start to now. + */ +export function getSessionStartDuration(event: PersistentEvent): number { + if (event.data?.sessionend) { + if (event.value) { + return event.value * 1000; + } + if (event.date) { + return new Date(event.data.sessionend).getTime() - new Date(event.date).getTime(); + } + return 0; + } + // If session is active, duration is from start to now + return Date.now() - new Date(event.date).getTime(); +} diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/leave-organization-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/leave-organization-dialog.svelte index 114995ee87..a2eab35d25 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/leave-organization-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/leave-organization-dialog.svelte @@ -21,7 +21,7 @@ Leave Organization - Are you sure you want to leave "{name}"? + Are you sure you want to leave "{name}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/remove-organization-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/remove-organization-dialog.svelte index d035325b56..fda168a705 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/remove-organization-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/components/dialogs/remove-organization-dialog.svelte @@ -21,7 +21,7 @@ Delete Organization - Are you sure you want to delete "{name}"? + Are you sure you want to delete "{name}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts index 0b98b0af57..cc5c1a6089 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/hooks/use-premium-feature.svelte.ts @@ -2,18 +2,26 @@ import { getContext, onDestroy } from 'svelte'; const NOTIFICATION_CONTEXT_KEY = 'organizationNotifications'; +interface Notification { + feature: string; + id: number; + message: string; + timestamp: number; + type: 'premium-feature'; +} + /** * Triggers a premium feature notification for the current organization. * @param featureName Feature name to display in the notification. */ export function usePremiumFeature(featureName?: string) { - const notifications = getContext(NOTIFICATION_CONTEXT_KEY) as undefined | { update: (fn: (n: any[]) => any[]) => void }; + const notifications = getContext(NOTIFICATION_CONTEXT_KEY) as undefined | { update: (fn: (n: Notification[]) => Notification[]) => void }; let notificationId: null | number = null; if (notifications && featureName) { const id = Date.now() + Math.floor(Math.random() * 10000); notificationId = id; - notifications.update((n: any[]) => [ + notifications.update((n: Notification[]) => [ ...n, { feature: featureName, @@ -26,8 +34,11 @@ export function usePremiumFeature(featureName?: string) { } onDestroy(() => { - if (!notifications || notificationId == null) return; - notifications.update((n: any[]) => n.filter((notif: any) => notif.id !== notificationId)); + if (!notifications || notificationId == null) { + return; + } + + notifications.update((n: Notification[]) => n.filter((notification: Notification) => notification.id !== notificationId)); notificationId = null; }); } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-config-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-config-dialog.svelte index 14ffc302c8..8fca64b345 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-config-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-config-dialog.svelte @@ -21,7 +21,7 @@ Delete Configuration Value - Are you sure you want to delete "{name}"? + Are you sure you want to delete "{name}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-dialog.svelte index 927171a7c2..b807f102c7 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/remove-project-dialog.svelte @@ -21,7 +21,7 @@ Delete Project - Are you sure you want to delete "{name}"? + Are you sure you want to delete "{name}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/reset-project-data-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/reset-project-data-dialog.svelte index a4274c1bc9..27dc7f6a7f 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/reset-project-data-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/dialogs/reset-project-data-dialog.svelte @@ -21,8 +21,8 @@ Reset Project Data - Are you sure you want to reset all project data for "{name}"? - This action cannot be undone and will permanently erase all events, stacks, and associated data. + Are you sure you want to reset all project data for "{name}"? This + action cannot be undone and will permanently erase all events, stacks, and associated data. diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/table/options.svelte.ts index b3a03fab5a..6bd286e214 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/projects/components/table/options.svelte.ts @@ -17,7 +17,7 @@ export function getColumns(mode: GetProjectsMode = enableHiding: false, header: 'Name', meta: { - class: 'w-[200px]' + class: 'w-50' } } ]; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/tokens/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/tokens/components/table/options.svelte.ts index 82f18c6562..1711101198 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/tokens/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/tokens/components/table/options.svelte.ts @@ -18,7 +18,7 @@ export function getColumns(): ColumnDef[] { enableSorting: false, header: 'API Key', meta: { - class: 'w-[180px]' + class: 'w-45' } }, { @@ -28,7 +28,7 @@ export function getColumns(): ColumnDef[] { enableSorting: false, header: 'Notes', meta: { - class: 'w-[200px]' + class: 'w-50' } }, { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/dialogs/remove-user-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/dialogs/remove-user-dialog.svelte index 58fa5748e1..cd393253de 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/dialogs/remove-user-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/dialogs/remove-user-dialog.svelte @@ -21,7 +21,7 @@ Remove User - Are you sure you want to remove "{name}"? + Are you sure you want to remove "{name}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/table/options.svelte.ts index c717ba9bc5..c41834776d 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/users/components/table/options.svelte.ts @@ -18,7 +18,7 @@ export function getColumns(organizationId: string): Colu enableSorting: false, header: 'Email Address', meta: { - class: 'w-[200px]' + class: 'w-50' } }, { @@ -28,7 +28,7 @@ export function getColumns(organizationId: string): Colu enableSorting: false, header: 'Name', meta: { - class: 'w-[200px]' + class: 'w-50' } }, { @@ -38,7 +38,7 @@ export function getColumns(organizationId: string): Colu enableSorting: false, header: 'Invited', meta: { - class: 'w-[100px]' + class: 'w-25' } } ]; diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/dialogs/remove-webhook-dialog.svelte b/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/dialogs/remove-webhook-dialog.svelte index 3c6f5bbf5e..ecda54b2da 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/dialogs/remove-webhook-dialog.svelte +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/dialogs/remove-webhook-dialog.svelte @@ -21,7 +21,7 @@ Delete Webhook - Are you sure you want to delete "{url}"? + Are you sure you want to delete "{url}"? diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/table/options.svelte.ts index 0ddb02f882..8c5b84c009 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/webhooks/components/table/options.svelte.ts @@ -17,7 +17,7 @@ export function getColumns(): ColumnDef[] { enableSorting: false, header: 'Url', meta: { - class: 'w-[200px]' + class: 'w-50' } }, { @@ -27,7 +27,7 @@ export function getColumns(): ColumnDef[] { enableSorting: false, header: 'Event Types', meta: { - class: 'w-[200px]' + class: 'w-50' } }, { diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/appearance/(components)/theme-preview.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/appearance/(components)/theme-preview.svelte index 30a65c3125..796ea9e376 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/appearance/(components)/theme-preview.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/account/appearance/(components)/theme-preview.svelte @@ -18,15 +18,15 @@
-
+
-
+
-
+
@@ -38,15 +38,15 @@
-
+
-
+
-
+
diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte index 2fd5a2ad59..190b267029 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/organization/[organizationId]/usage/+page.svelte @@ -84,7 +84,7 @@ {#if organizationQuery.isLoading}
- +
{:else if organizationQuery.error} diff --git a/src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte b/src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte index 97b665b870..14474fec0d 100644 --- a/src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte +++ b/src/Exceptionless.Web/ClientApp/src/routes/(app)/project/[projectId]/usage/+page.svelte @@ -112,7 +112,7 @@ {#if projectQuery.isLoading || organizationQuery.isLoading}
- +
{:else if projectQuery.error || organizationQuery.error} @@ -145,7 +145,7 @@

- + Date: Sat, 14 Feb 2026 15:29:02 -0600 Subject: [PATCH 12/12] Fixed linting --- .../ClientApp/src/lib/features/organizations/api.svelte.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts index 10dc145daf..b3be2e34c2 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/organizations/api.svelte.ts @@ -359,10 +359,6 @@ export function postOrganization() { onSuccess: (organization: ViewOrganization) => { queryClient.setQueryData(queryKeys.id(organization.id, 'stats'), organization); queryClient.setQueryData(queryKeys.id(organization.id, undefined), organization); - // Invalidate organizations list so it includes the new org - queryClient.invalidateQueries({ queryKey: queryKeys.type }); - // Invalidate user query since organization_ids changed on the backend - queryClient.invalidateQueries({ queryKey: userQueryKeys }); } })); }