Skip to content

Commit f86c8e4

Browse files
authored
feat(explore): Handle aggregate fields in saved explore queries (#92999)
This starts using aggregate fields for saved explore queries instead of group by and visualizes.
1 parent 4bb18d1 commit f86c8e4

File tree

6 files changed

+170
-46
lines changed

6 files changed

+170
-46
lines changed

static/app/views/explore/contexts/pageParamsContext/aggregateFields.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface GroupBy {
1818
groupBy: string;
1919
}
2020

21-
function _isBaseVisualize(value: any): value is BaseVisualize {
21+
function isBaseVisualize(value: any): value is BaseVisualize {
2222
return (
2323
typeof value === 'object' &&
2424
Array.isArray(value.yAxes) &&
@@ -77,7 +77,7 @@ export function getAggregateFieldsFromLocation(
7777
if (isGroupBy(groupByOrBaseVisualize)) {
7878
aggregateFields.push(groupByOrBaseVisualize);
7979
hasGroupBys = true;
80-
} else if (_isBaseVisualize(groupByOrBaseVisualize)) {
80+
} else if (isBaseVisualize(groupByOrBaseVisualize)) {
8181
for (const yAxis of groupByOrBaseVisualize.yAxes) {
8282
aggregateFields.push(
8383
new Visualize([yAxis], {
@@ -112,7 +112,7 @@ export function updateLocationWithAggregateFields(
112112
) {
113113
if (defined(aggregateFields)) {
114114
location.query.aggregateField = aggregateFields.map(aggregateField => {
115-
if (_isBaseVisualize(aggregateField)) {
115+
if (isBaseVisualize(aggregateField)) {
116116
return JSON.stringify(Visualize.fromJSON(aggregateField).toJSON());
117117
}
118118
return JSON.stringify(aggregateField);

static/app/views/explore/hooks/useExploreAggregatesTable.tsx

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import EventView from 'sentry/utils/discover/eventView';
66
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
77
import usePageFilters from 'sentry/utils/usePageFilters';
88
import {
9+
useExploreAggregateFields,
910
useExploreDataset,
10-
useExploreGroupBys,
1111
useExploreSortBys,
12-
useExploreVisualizes,
1312
} from 'sentry/views/explore/contexts/pageParamsContext';
13+
import {isGroupBy} from 'sentry/views/explore/contexts/pageParamsContext/aggregateFields';
1414
import {formatSort} from 'sentry/views/explore/contexts/pageParamsContext/sortBys';
1515
import type {SpansRPCQueryExtras} from 'sentry/views/explore/hooks/useProgressiveQuery';
1616
import {useProgressiveQuery} from 'sentry/views/explore/hooks/useProgressiveQuery';
@@ -60,33 +60,32 @@ function useExploreAggregatesTableImp({
6060
const {selection} = usePageFilters();
6161

6262
const dataset = useExploreDataset();
63-
const groupBys = useExploreGroupBys();
63+
const aggregateFields = useExploreAggregateFields();
6464
const sorts = useExploreSortBys();
65-
const visualizes = useExploreVisualizes();
6665

6766
const fields = useMemo(() => {
6867
// When rendering the table, we want the group bys first
6968
// then the aggregates.
7069
const allFields: string[] = [];
7170

72-
for (const groupBy of groupBys) {
73-
if (allFields.includes(groupBy)) {
74-
continue;
75-
}
76-
allFields.push(groupBy);
77-
}
78-
79-
for (const visualize of visualizes) {
80-
for (const yAxis of visualize.yAxes) {
81-
if (allFields.includes(yAxis)) {
71+
for (const aggregateField of aggregateFields) {
72+
if (isGroupBy(aggregateField)) {
73+
if (allFields.includes(aggregateField.groupBy)) {
8274
continue;
8375
}
84-
allFields.push(yAxis);
76+
allFields.push(aggregateField.groupBy);
77+
} else {
78+
for (const yAxis of aggregateField.yAxes) {
79+
if (allFields.includes(yAxis)) {
80+
continue;
81+
}
82+
allFields.push(yAxis);
83+
}
8584
}
8685
}
8786

8887
return allFields.filter(Boolean);
89-
}, [groupBys, visualizes]);
88+
}, [aggregateFields]);
9089

9190
const eventView = useMemo(() => {
9291
const search = new MutableSearch(query);

static/app/views/explore/hooks/useGetSavedQueries.tsx

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,78 @@
1-
import {useCallback} from 'react';
1+
import {useCallback, useMemo} from 'react';
22

33
import type {Actor} from 'sentry/types/core';
44
import {defined} from 'sentry/utils';
55
import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
66
import useOrganization from 'sentry/utils/useOrganization';
77
import type {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
88

9-
type Query = {
9+
export type RawGroupBy = {
10+
groupBy: string;
11+
};
12+
13+
function isRawGroupBy(value: any): value is RawGroupBy {
14+
return typeof value === 'object' && typeof value.groupBy === 'string';
15+
}
16+
17+
export type RawVisualize = {
18+
yAxes: string[];
19+
chartType?: number;
20+
};
21+
22+
export function isRawVisualize(value: any): value is RawVisualize {
23+
return (
24+
typeof value === 'object' &&
25+
Array.isArray(value.yAxes) &&
26+
value.yAxes.every((v: any) => typeof v === 'string')
27+
);
28+
}
29+
30+
type ReadableQuery = {
1031
fields: string[];
11-
groupby: string[];
1232
mode: Mode;
1333
orderby: string;
1434
query: string;
15-
visualize: Array<{
16-
chartType: number;
17-
yAxes: string[];
18-
}>;
35+
36+
// a query can have either
37+
// - `aggregateField` which contains a list of group bys and visualizes merged together
38+
// - `groupby` and `visualize` which contains the group bys and visualizes separately
39+
aggregateField?: Array<RawGroupBy | RawVisualize>;
40+
groupby?: string[];
41+
visualize?: RawVisualize[];
1942
};
2043

44+
class Query {
45+
fields: string[];
46+
mode: Mode;
47+
orderby: string;
48+
query: string;
49+
50+
aggregateField: Array<RawGroupBy | RawVisualize>;
51+
groupby: string[];
52+
visualize: RawVisualize[];
53+
54+
constructor(query: ReadableQuery) {
55+
this.fields = query.fields;
56+
this.mode = query.mode;
57+
this.orderby = query.orderby;
58+
this.query = query.query;
59+
60+
// for compatibility, we ensure that aggregate fields, group bys and visualizes are all populated
61+
// we ensure that group bys + visualizes = aggregate fields
62+
this.groupby =
63+
query.aggregateField
64+
?.filter<RawGroupBy>(isRawGroupBy)
65+
.map(groupBy => groupBy.groupBy) ??
66+
query.groupby ??
67+
[];
68+
this.visualize =
69+
query.aggregateField?.filter<RawVisualize>(isRawVisualize) ?? query.visualize ?? [];
70+
this.aggregateField = defined(query.aggregateField)
71+
? query.aggregateField
72+
: [...this.groupby.map(groupBy => ({groupBy})), ...this.visualize];
73+
}
74+
}
75+
2176
export type SortOption =
2277
| 'name'
2378
| '-name'
@@ -30,7 +85,7 @@ export type SortOption =
3085
| 'mostStarred';
3186

3287
// Comes from ExploreSavedQueryModelSerializer
33-
export type SavedQuery = {
88+
type ReadableSavedQuery = {
3489
dateAdded: string;
3590
dateUpdated: string;
3691
id: number;
@@ -39,7 +94,7 @@ export type SavedQuery = {
3994
name: string;
4095
position: number | null;
4196
projects: number[];
42-
query: [Query, ...Query[]];
97+
query: [ReadableQuery, ...ReadableQuery[]];
4398
queryDataset: string;
4499
starred: boolean;
45100
createdBy?: Actor;
@@ -50,6 +105,49 @@ export type SavedQuery = {
50105
start?: string;
51106
};
52107

108+
export class SavedQuery {
109+
dateAdded: string;
110+
dateUpdated: string;
111+
id: number;
112+
interval: string;
113+
lastVisited: string;
114+
name: string;
115+
position: number | null;
116+
projects: number[];
117+
query: [Query, ...Query[]];
118+
queryDataset: string;
119+
starred: boolean;
120+
createdBy?: Actor;
121+
end?: string;
122+
environment?: string[];
123+
isPrebuilt?: boolean;
124+
range?: string;
125+
start?: string;
126+
127+
constructor(savedQuery: ReadableSavedQuery) {
128+
this.dateAdded = savedQuery.dateAdded;
129+
this.dateUpdated = savedQuery.dateUpdated;
130+
this.id = savedQuery.id;
131+
this.interval = savedQuery.interval;
132+
this.lastVisited = savedQuery.lastVisited;
133+
this.name = savedQuery.name;
134+
this.position = savedQuery.position;
135+
this.projects = savedQuery.projects;
136+
this.query = [
137+
new Query(savedQuery.query[0]),
138+
...savedQuery.query.slice(1).map(q => new Query(q)),
139+
];
140+
this.queryDataset = savedQuery.queryDataset;
141+
this.starred = savedQuery.starred;
142+
this.createdBy = savedQuery.createdBy;
143+
this.end = savedQuery.end;
144+
this.environment = savedQuery.environment;
145+
this.isPrebuilt = savedQuery.isPrebuilt;
146+
this.range = savedQuery.range;
147+
this.start = savedQuery.start;
148+
}
149+
}
150+
53151
type Props = {
54152
cursor?: string;
55153
exclude?: 'owned' | 'shared';
@@ -69,7 +167,7 @@ export function useGetSavedQueries({
69167
}: Props) {
70168
const organization = useOrganization();
71169

72-
const {data, isLoading, getResponseHeader, ...rest} = useApiQuery<SavedQuery[]>(
170+
const {data, isLoading, getResponseHeader, ...rest} = useApiQuery<ReadableSavedQuery[]>(
73171
[
74172
`/organizations/${organization.slug}/explore/saved/`,
75173
{
@@ -90,7 +188,8 @@ export function useGetSavedQueries({
90188

91189
const pageLinks = getResponseHeader?.('Link');
92190

93-
return {data, isLoading, pageLinks, ...rest};
191+
const savedQueries = useMemo(() => data?.map(q => new SavedQuery(q)), [data]);
192+
return {data: savedQueries, isLoading, pageLinks, ...rest};
94193
}
95194

96195
export function useInvalidateSavedQueries() {
@@ -106,14 +205,15 @@ export function useInvalidateSavedQueries() {
106205

107206
export function useGetSavedQuery(id?: string) {
108207
const organization = useOrganization();
109-
const {data, isLoading, ...rest} = useApiQuery<SavedQuery>(
208+
const {data, isLoading, ...rest} = useApiQuery<ReadableSavedQuery>(
110209
[`/organizations/${organization.slug}/explore/saved/${id}/`],
111210
{
112211
staleTime: 0,
113212
enabled: defined(id),
114213
}
115214
);
116-
return {data, isLoading, ...rest};
215+
const savedQuery = useMemo(() => (defined(data) ? new SavedQuery(data) : data), [data]);
216+
return {data: savedQuery, isLoading, ...rest};
117217
}
118218

119219
export function useInvalidateSavedQuery(id?: string) {

static/app/views/explore/hooks/useSaveQuery.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ export function useSaveQuery() {
2727
const invalidateSavedQueries = useInvalidateSavedQueries();
2828
const invalidateSavedQuery = useInvalidateSavedQuery(id);
2929

30-
const visualize = visualizes.map(v => v.toJSON());
31-
3230
const data = useMemo(() => {
3331
return {
3432
name: title,
@@ -45,15 +43,15 @@ export function useSaveQuery() {
4543
orderby: sortBys[0] ? encodeSort(sortBys[0]) : undefined,
4644
groupby: groupBys.filter(groupBy => groupBy !== ''),
4745
query: query ?? '',
48-
visualize,
46+
visualize: visualizes.map(visualize => visualize.toJSON()),
4947
mode,
5048
},
5149
],
5250
};
5351
}, [
5452
groupBys,
5553
sortBys,
56-
visualize,
54+
visualizes,
5755
fields,
5856
query,
5957
mode,

static/app/views/explore/utils.tsx

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,22 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch';
2222
import {determineSeriesSampleCountAndIsSampled} from 'sentry/views/alerts/rules/metric/utils/determineSeriesSampleCount';
2323
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';
2424
import {newExploreTarget} from 'sentry/views/explore/contexts/pageParamsContext';
25+
import type {GroupBy} from 'sentry/views/explore/contexts/pageParamsContext/aggregateFields';
26+
import {isGroupBy} from 'sentry/views/explore/contexts/pageParamsContext/aggregateFields';
2527
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
2628
import type {
2729
BaseVisualize,
2830
Visualize,
2931
} from 'sentry/views/explore/contexts/pageParamsContext/visualizes';
30-
import type {SavedQuery} from 'sentry/views/explore/hooks/useGetSavedQueries';
32+
import type {
33+
RawGroupBy,
34+
RawVisualize,
35+
SavedQuery,
36+
} from 'sentry/views/explore/hooks/useGetSavedQueries';
37+
import {isRawVisualize} from 'sentry/views/explore/hooks/useGetSavedQueries';
3138
import type {ReadableExploreQueryParts} from 'sentry/views/explore/multiQueryMode/locationUtils';
3239
import type {ChartType} from 'sentry/views/insights/common/components/chart';
40+
import {isChartType} from 'sentry/views/insights/common/components/chart';
3341
import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
3442
import {makeTracesPathname} from 'sentry/views/traces/pathnames';
3543

@@ -38,6 +46,7 @@ export function getExploreUrl({
3846
selection,
3947
interval,
4048
mode,
49+
aggregateField,
4150
visualize,
4251
query,
4352
groupBy,
@@ -47,7 +56,7 @@ export function getExploreUrl({
4756
title,
4857
}: {
4958
organization: Organization;
50-
visualize: BaseVisualize[];
59+
aggregateField?: Array<GroupBy | BaseVisualize>;
5160
field?: string[];
5261
groupBy?: string[];
5362
id?: number;
@@ -57,6 +66,7 @@ export function getExploreUrl({
5766
selection?: PageFilters;
5867
sort?: string;
5968
title?: string;
69+
visualize?: BaseVisualize[];
6070
}) {
6171
const {start, end, period: statsPeriod, utc} = selection?.datetime ?? {};
6272
const {environments, projects} = selection ?? {};
@@ -69,7 +79,8 @@ export function getExploreUrl({
6979
interval,
7080
mode,
7181
query,
72-
visualize: visualize.map(v => JSON.stringify(v)),
82+
aggregateField: aggregateField?.map(v => JSON.stringify(v)),
83+
visualize: visualize?.map(v => JSON.stringify(v)),
7384
groupBy,
7485
sort,
7586
field,
@@ -97,13 +108,25 @@ export function getExploreUrlFromSavedQueryUrl({
97108
return getExploreMultiQueryUrl({
98109
organization,
99110
...savedQuery,
100-
queries: savedQuery.query.map(q => ({
101-
...q,
102-
chartType: q.visualize[0]?.chartType as ChartType, // Multi Query View only supports a single visualize per query
103-
yAxes: q.visualize[0]?.yAxes ?? [],
104-
groupBys: q.groupby,
105-
sortBys: decodeSorts(q.orderby),
106-
})),
111+
queries: savedQuery.query.map(q => {
112+
const groupBys: string[] | undefined =
113+
q.aggregateField
114+
?.filter<RawGroupBy>(isGroupBy)
115+
?.map(groupBy => groupBy.groupBy) ?? q.groupby;
116+
const visualize: RawVisualize | undefined =
117+
q.aggregateField?.find<RawVisualize>(isRawVisualize) ?? q.visualize?.[0];
118+
const chartType: ChartType | undefined = isChartType(visualize?.chartType)
119+
? visualize.chartType
120+
: undefined;
121+
122+
return {
123+
...q,
124+
chartType,
125+
yAxes: (visualize?.yAxes ?? []).slice(),
126+
groupBys: groupBys ?? [],
127+
sortBys: decodeSorts(q.orderby),
128+
};
129+
}),
107130
title: savedQuery.name,
108131
selection: {
109132
datetime: {

0 commit comments

Comments
 (0)