Skip to content

Commit 9fd7d34

Browse files
committed
Added Count Result Aggregation models and helpers from foundatio. This allows us fully typed aggregations via the api.
1 parent ebb8227 commit 9fd7d34

File tree

12 files changed

+307
-12
lines changed

12 files changed

+307
-12
lines changed

src/Exceptionless.Web/ClientApp/src/lib/features/events/api.svelte.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { WebSocketMessageValue } from '$features/websockets/models';
2+
import type { CountResult } from '$shared/models';
23

34
import { accessToken } from '$features/auth/index.svelte';
45
import { type ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
@@ -17,21 +18,35 @@ export async function invalidatePersistentEventQueries(queryClient: QueryClient,
1718
}
1819

1920
if (project_id) {
20-
await queryClient.invalidateQueries({ queryKey: queryKeys.projects(project_id) });
21+
await queryClient.invalidateQueries({ queryKey: queryKeys.projectsCount(project_id) });
2122
}
2223

2324
if (!id && !stack_id) {
2425
await queryClient.invalidateQueries({ queryKey: queryKeys.type });
26+
} else {
27+
await queryClient.invalidateQueries({ queryKey: queryKeys.count() });
2528
}
2629
}
2730

2831
export const queryKeys = {
32+
count: () => [...queryKeys.type, 'count'] as const,
2933
id: (id: string | undefined) => [...queryKeys.type, id] as const,
30-
projects: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
34+
projectsCount: (id: string | undefined) => [...queryKeys.type, 'projects', id] as const,
3135
stacks: (id: string | undefined) => [...queryKeys.type, 'stacks', id] as const,
36+
stacksCount: (id: string | undefined) => [...queryKeys.stacks(id), 'count'] as const,
3237
type: ['PersistentEvent'] as const
3338
};
3439

40+
export interface GetCountRequest {
41+
params?: {
42+
aggregations?: string;
43+
filter?: string;
44+
mode?: 'stack_new';
45+
offset?: string;
46+
time?: string;
47+
};
48+
}
49+
3550
export interface GetEventRequest {
3651
route: {
3752
id: string | undefined;
@@ -93,6 +108,25 @@ export interface GetStackEventsRequest {
93108
};
94109
}
95110

111+
export function getCountQuery(request: GetCountRequest) {
112+
const queryClient = useQueryClient();
113+
114+
return createQuery<CountResult, ProblemDetails>(() => ({
115+
enabled: () => !!accessToken.value,
116+
queryClient,
117+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
118+
const client = useFetchClient();
119+
const response = await client.getJSON<CountResult>('events/count', {
120+
params: request.params,
121+
signal
122+
});
123+
124+
return response.data!;
125+
},
126+
queryKey: queryKeys.count()
127+
}));
128+
}
129+
96130
export function getEventQuery(request: GetEventRequest) {
97131
return createQuery<PersistentEvent, ProblemDetails>(() => ({
98132
enabled: () => !!accessToken.value && !!request.route.id,
@@ -111,19 +145,43 @@ export function getEventQuery(request: GetEventRequest) {
111145
export function getProjectCountQuery(request: GetProjectCountRequest) {
112146
const queryClient = useQueryClient();
113147

114-
return createQuery<PersistentEvent[], ProblemDetails>(() => ({
148+
return createQuery<CountResult, ProblemDetails>(() => ({
115149
enabled: () => !!accessToken.value && !!request.route.projectId,
116150
queryClient,
117151
queryFn: async ({ signal }: { signal: AbortSignal }) => {
118152
const client = useFetchClient();
119-
const response = await client.getJSON<PersistentEvent[]>(`/projects/${request.route.projectId}/events/count`, {
153+
const response = await client.getJSON<CountResult>(`/projects/${request.route.projectId}/events/count`, {
120154
params: request.params,
121155
signal
122156
});
123157

124158
return response.data!;
125159
},
126-
queryKey: queryKeys.projects(request.route.projectId)
160+
queryKey: queryKeys.projectsCount(request.route.projectId)
161+
}));
162+
}
163+
164+
export function getStackCountQuery(request: GetStackCountRequest) {
165+
const queryClient = useQueryClient();
166+
167+
return createQuery<CountResult, ProblemDetails>(() => ({
168+
enabled: () => !!accessToken.value && !!request.route.stackId,
169+
queryClient,
170+
queryFn: async ({ signal }: { signal: AbortSignal }) => {
171+
const client = useFetchClient();
172+
const response = await client.getJSON<CountResult>('events/count', {
173+
params: {
174+
...request.params,
175+
filter: request.params?.filter?.includes(`stack:${request.route.stackId}`)
176+
? request.params.filter
177+
: [request.params?.filter, `stack:${request.route.stackId}`].filter(Boolean).join(' ')
178+
},
179+
signal
180+
});
181+
182+
return response.data!;
183+
},
184+
queryKey: queryKeys.stacksCount(request.route.stackId)
127185
}));
128186
}
129187

src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import NumberFormatter from '$comp/formatters/Number.svelte';
44
import TimeAgo from '$comp/formatters/TimeAgo.svelte';
55
import { Checkbox } from '$comp/ui/checkbox';
66
import { nameof } from '$lib/utils';
7-
import { DEFAULT_LIMIT } from '$shared/api.svelte';
7+
import { DEFAULT_LIMIT } from '$shared/api/api.svelte';
88
import { persisted } from '$shared/persisted.svelte';
99
import {
1010
type ColumnDef,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type {
2+
BucketAggregate,
3+
DateHistogramBucket,
4+
ExtendedStatsAggregate,
5+
IAggregate,
6+
IBucket,
7+
KeyedBucket,
8+
MultiBucketAggregate,
9+
ObjectValueAggregate,
10+
PercentileItem,
11+
PercentilesAggregate,
12+
SingleBucketAggregate,
13+
StatsAggregate,
14+
TermsAggregate,
15+
TopHitsAggregate,
16+
ValueAggregate
17+
} from '../models';
18+
19+
export function average(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
20+
return tryGet<ValueAggregate>(aggregations, key);
21+
}
22+
23+
export function cardinality(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
24+
return tryGet<ValueAggregate>(aggregations, key);
25+
}
26+
27+
export function dateHistogram(aggregations: Record<string, IAggregate> | undefined, key: string): MultiBucketAggregate<DateHistogramBucket> | undefined {
28+
return getMultiBucketAggregate<DateHistogramBucket>(aggregations, key);
29+
}
30+
31+
export function extendedStats(aggregations: Record<string, IAggregate> | undefined, key: string): ExtendedStatsAggregate | undefined {
32+
return tryGet<ExtendedStatsAggregate>(aggregations, key);
33+
}
34+
35+
export function geoHash(aggregations: Record<string, IAggregate> | undefined, key: string): MultiBucketAggregate<KeyedBucket<string>> | undefined {
36+
return getMultiKeyedBucketAggregate<string>(aggregations, key);
37+
}
38+
39+
export function getPercentile(agg: PercentilesAggregate, percentile: number): PercentileItem | undefined {
40+
return agg.items.find((i) => i.percentile === percentile); // Checked
41+
}
42+
43+
export function max<T = number>(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate<T> {
44+
return tryGet<ValueAggregate<T>>(aggregations, key);
45+
}
46+
47+
export function metric(aggregations: Record<string, IAggregate> | undefined, key: string): ObjectValueAggregate | undefined {
48+
const valueMetric = tryGet<ValueAggregate>(aggregations, key);
49+
if (valueMetric) {
50+
return <ObjectValueAggregate>{
51+
data: valueMetric.data,
52+
value: valueMetric.value
53+
};
54+
}
55+
56+
return tryGet<ObjectValueAggregate>(aggregations, key);
57+
}
58+
59+
export function min<T = number>(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate<T> {
60+
return tryGet<ValueAggregate<T>>(aggregations, key);
61+
}
62+
63+
export function missing(aggregations: Record<string, IAggregate> | undefined, key: string): SingleBucketAggregate | undefined {
64+
return tryGet<SingleBucketAggregate>(aggregations, key);
65+
}
66+
67+
export function percentiles(aggregations: Record<string, IAggregate> | undefined, key: string): PercentilesAggregate | undefined {
68+
return tryGet<PercentilesAggregate>(aggregations, key);
69+
}
70+
71+
export function stats(aggregations: Record<string, IAggregate> | undefined, key: string): StatsAggregate | undefined {
72+
return tryGet<StatsAggregate>(aggregations, key);
73+
}
74+
75+
export function sum(aggregations: Record<string, IAggregate> | undefined, key: string): undefined | ValueAggregate {
76+
return tryGet<ValueAggregate>(aggregations, key);
77+
}
78+
79+
export function terms<TKey = string>(aggregations: Record<string, IAggregate> | undefined, key: string): TermsAggregate<TKey> | undefined {
80+
const bucket = tryGet<BucketAggregate>(aggregations, key);
81+
if (!bucket) {
82+
return;
83+
}
84+
85+
return <TermsAggregate<TKey>>{
86+
buckets: getKeyedBuckets<TKey>(bucket.items),
87+
data: bucket.data
88+
};
89+
}
90+
91+
export function topHits<T = unknown>(aggregations: Record<string, IAggregate>): TopHitsAggregate<T> | undefined {
92+
return tryGet<TopHitsAggregate<T>>(aggregations, 'tophits');
93+
}
94+
95+
function getKeyedBuckets<TKey>(items: IBucket[]): KeyedBucket<TKey>[] {
96+
return items
97+
.filter((bucket): bucket is KeyedBucket<TKey> => 'key' in bucket)
98+
.map((bucket) => ({
99+
aggregations: bucket.aggregations,
100+
key: bucket.key, // NOTE: May have to convert to proper type
101+
key_as_string: bucket.key_as_string,
102+
total: bucket.total
103+
}));
104+
}
105+
106+
function getMultiBucketAggregate<TBucket extends IBucket>(
107+
aggregations: Record<string, IAggregate> | undefined,
108+
key: string
109+
): MultiBucketAggregate<TBucket> | undefined {
110+
const bucket = tryGet<BucketAggregate>(aggregations, key);
111+
if (!bucket) {
112+
return;
113+
}
114+
115+
return <MultiBucketAggregate<TBucket>>{
116+
buckets: bucket.items,
117+
data: bucket.data
118+
};
119+
}
120+
121+
function getMultiKeyedBucketAggregate<TKey>(
122+
aggregations: Record<string, IAggregate> | undefined,
123+
key: string
124+
): MultiBucketAggregate<KeyedBucket<TKey>> | undefined {
125+
const bucket = tryGet<BucketAggregate>(aggregations, key);
126+
if (!bucket) {
127+
return;
128+
}
129+
130+
return <MultiBucketAggregate<KeyedBucket<TKey>>>{
131+
buckets: getKeyedBuckets<TKey>(bucket.items),
132+
data: bucket.data
133+
};
134+
}
135+
136+
function tryGet<TAggregate extends IAggregate>(aggregations: Record<string, IAggregate> | undefined, key: string): TAggregate | undefined {
137+
return aggregations?.[key] as TAggregate;
138+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { IAggregate } from '.';
2+
3+
export interface AggregationsHelper {
4+
aggregations: Record<string, IAggregate>;
5+
}
6+
7+
export interface BucketAggregate extends IAggregate {
8+
items: IBucket[];
9+
total: number;
10+
}
11+
12+
export interface BucketAggregateBase extends AggregationsHelper, IAggregate {
13+
aggregations: Record<string, IAggregate>;
14+
}
15+
16+
export interface BucketBase extends AggregationsHelper, IBucket {
17+
aggregations: Record<string, IAggregate>;
18+
}
19+
20+
export interface DateHistogramBucket extends KeyedBucket<number> {
21+
date: string; // This needs to be converted to a date.
22+
}
23+
24+
export interface ExtendedStatsAggregate extends StatsAggregate {
25+
std_deviation?: number;
26+
std_deviation_bounds?: StandardDeviationBounds;
27+
sum_of_squares?: number;
28+
variance?: number;
29+
}
30+
31+
export interface IBucket {
32+
data?: Record<string, unknown>;
33+
}
34+
35+
export interface KeyedBucket<T> extends BucketBase {
36+
key: T;
37+
key_as_string: string;
38+
total?: number;
39+
}
40+
41+
export type MetricAggregateBase = IAggregate;
42+
43+
export interface MultiBucketAggregate<TBucket extends IBucket> extends BucketAggregateBase {
44+
buckets: TBucket[];
45+
}
46+
47+
export interface ObjectValueAggregate extends MetricAggregateBase {
48+
value: unknown;
49+
}
50+
51+
export interface PercentileItem {
52+
percentile: number;
53+
value?: number;
54+
}
55+
56+
export interface PercentilesAggregate extends MetricAggregateBase {
57+
items: PercentileItem[];
58+
}
59+
60+
export interface RangeBucket extends BucketBase {
61+
from?: number;
62+
from_as_string?: string;
63+
key: string;
64+
to?: number;
65+
to_as_string?: string;
66+
total: number;
67+
}
68+
69+
export interface SingleBucketAggregate extends BucketAggregateBase {
70+
total: number;
71+
}
72+
73+
export interface StandardDeviationBounds {
74+
lower?: number;
75+
upper?: number;
76+
}
77+
78+
export interface StatsAggregate extends MetricAggregateBase {
79+
average?: number;
80+
count: number;
81+
max?: number;
82+
min?: number;
83+
sum?: number;
84+
}
85+
86+
export type TermsAggregate<TKey> = MultiBucketAggregate<KeyedBucket<TKey>>;
87+
88+
export interface TopHitsAggregate<T> extends MetricAggregateBase {
89+
hits: T[];
90+
max_score?: number;
91+
total: number;
92+
}
93+
94+
export interface ValueAggregate<T = number> extends MetricAggregateBase {
95+
value: T;
96+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { CountResult, type IAggregate } from '$generated/api';
2+
3+
export * from './aggregations';

src/Exceptionless.Web/ClientApp/src/routes/(app)/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import EventsDataTable from '$features/events/components/table/EventsDataTable.svelte';
1313
import { getTableContext } from '$features/events/components/table/options.svelte';
1414
import { ChangeType, type WebSocketMessageValue } from '$features/websockets/models';
15-
import { useFetchClientStatus } from '$shared/api.svelte';
15+
import { useFetchClientStatus } from '$shared/api/api.svelte';
1616
import { persisted } from '$shared/persisted.svelte';
1717
import { type FetchClientResponse, useFetchClient } from '@exceptionless/fetchclient';
1818
import { createTable } from '@tanstack/svelte-table';

src/Exceptionless.Web/ClientApp/src/routes/(app)/account/notifications/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Button } from '$comp/ui/button';
77
import { Separator } from '$comp/ui/separator';
88
import { User } from '$features/users/models';
9-
import { useFetchClientStatus } from '$shared/api.svelte';
9+
import { useFetchClientStatus } from '$shared/api/api.svelte';
1010
import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
1111
1212
const data = $state(new User());

src/Exceptionless.Web/ClientApp/src/routes/(app)/account/security/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
microsoftClientId
1818
} from '$features/auth/index.svelte';
1919
import { User } from '$features/users/models';
20-
import { useFetchClientStatus } from '$shared/api.svelte';
20+
import { useFetchClientStatus } from '$shared/api/api.svelte';
2121
import { ProblemDetails, useFetchClient } from '@exceptionless/fetchclient';
2222
import IconFacebook from '~icons/mdi/facebook';
2323
import IconGitHub from '~icons/mdi/github';

0 commit comments

Comments
 (0)