Skip to content

Commit 92ada4f

Browse files
authored
chore(explore): Add in cross event query param (#103666)
This PR adds in the boilerplate for adding in a new `crossEvents` query param to manage the state of displaying which and how many cross event search bars. Ticket: EXP-600
1 parent f425934 commit 92ada4f

File tree

7 files changed

+221
-4
lines changed

7 files changed

+221
-4
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {useMemo, type ReactNode} from 'react';
2+
3+
import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary';
4+
5+
import {useResettableState} from 'sentry/utils/useResettableState';
6+
import {
7+
defaultAggregateFields,
8+
defaultAggregateSortBys,
9+
defaultFields,
10+
defaultQuery,
11+
defaultSortBys,
12+
} from 'sentry/views/explore/metrics/metricQuery';
13+
import {
14+
QueryParamsContextProvider,
15+
useQueryParamsCrossEvents,
16+
useSetQueryParamsCrossEvents,
17+
} from 'sentry/views/explore/queryParams/context';
18+
import {defaultCursor} from 'sentry/views/explore/queryParams/cursor';
19+
import {Mode} from 'sentry/views/explore/queryParams/mode';
20+
import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams';
21+
22+
const mockSetQueryParams = jest.fn();
23+
24+
function Wrapper({children}: {children: ReactNode}) {
25+
const [query] = useResettableState(defaultQuery);
26+
27+
const readableQueryParams = useMemo(
28+
() =>
29+
new ReadableQueryParams({
30+
aggregateCursor: defaultCursor(),
31+
aggregateFields: defaultAggregateFields(),
32+
aggregateSortBys: defaultAggregateSortBys(defaultAggregateFields()),
33+
cursor: defaultCursor(),
34+
extrapolate: true,
35+
fields: defaultFields(),
36+
mode: Mode.AGGREGATE,
37+
query,
38+
sortBys: defaultSortBys(defaultFields()),
39+
crossEvents: [{query: 'foo', type: 'spans'}],
40+
}),
41+
[query]
42+
);
43+
44+
return (
45+
<QueryParamsContextProvider
46+
isUsingDefaultFields={false}
47+
queryParams={readableQueryParams}
48+
setQueryParams={mockSetQueryParams}
49+
shouldManageFields={false}
50+
>
51+
{children}
52+
</QueryParamsContextProvider>
53+
);
54+
}
55+
56+
describe('QueryParamsContext', () => {
57+
describe('crossEvents', () => {
58+
describe('useQueryParamsCrossEvents', () => {
59+
it('should return the crossEvents', () => {
60+
const {result} = renderHookWithProviders(() => useQueryParamsCrossEvents(), {
61+
additionalWrapper: Wrapper,
62+
});
63+
64+
expect(result.current).toEqual([{query: 'foo', type: 'spans'}]);
65+
});
66+
});
67+
68+
describe('useSetQueryParamsCrossEvents', () => {
69+
it('should set the crossEvents', () => {
70+
renderHookWithProviders(
71+
() => {
72+
const setCrossEvents = useSetQueryParamsCrossEvents();
73+
setCrossEvents([{query: 'bar', type: 'logs'}]);
74+
return useQueryParamsCrossEvents();
75+
},
76+
{additionalWrapper: Wrapper}
77+
);
78+
79+
expect(mockSetQueryParams).toHaveBeenCalled();
80+
expect(mockSetQueryParams).toHaveBeenCalledWith({
81+
crossEvents: [{query: 'bar', type: 'logs'}],
82+
});
83+
});
84+
});
85+
});
86+
});

static/app/views/explore/queryParams/context.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
AggregateField,
1414
WritableAggregateField,
1515
} from 'sentry/views/explore/queryParams/aggregateField';
16+
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
1617
import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy';
1718
import {updateNullableLocation} from 'sentry/views/explore/queryParams/location';
1819
import {deriveUpdatedManagedFields} from 'sentry/views/explore/queryParams/managedFields';
@@ -462,3 +463,19 @@ export function useSetQueryParamsSavedQuery() {
462463
[location, navigate]
463464
);
464465
}
466+
467+
export function useQueryParamsCrossEvents() {
468+
const queryParams = useQueryParams();
469+
return queryParams.crossEvents;
470+
}
471+
472+
export function useSetQueryParamsCrossEvents() {
473+
const setQueryParams = useSetQueryParams();
474+
475+
return useCallback(
476+
(crossEvents: CrossEvent[]) => {
477+
setQueryParams({crossEvents});
478+
},
479+
[setQueryParams]
480+
);
481+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type {Location} from 'history';
2+
3+
import {defined} from 'sentry/utils';
4+
5+
type CrossEventType = 'logs' | 'spans' | 'metrics';
6+
7+
export interface CrossEvent {
8+
query: string;
9+
type: CrossEventType;
10+
}
11+
12+
export function getCrossEventsFromLocation(
13+
location: Location,
14+
key: string
15+
): CrossEvent[] | undefined {
16+
let json: any;
17+
18+
if (!defined(location.query?.[key]) || Array.isArray(location.query?.[key])) {
19+
return undefined;
20+
}
21+
22+
try {
23+
json = JSON.parse(location.query?.[key]);
24+
} catch {
25+
return undefined;
26+
}
27+
28+
if (Array.isArray(json) && json.every(isCrossEvent)) {
29+
return json;
30+
}
31+
32+
return undefined;
33+
}
34+
35+
export function isCrossEventType(value: string): value is CrossEventType {
36+
return value === 'logs' || value === 'spans' || value === 'metrics';
37+
}
38+
39+
function isCrossEvent(value: any): value is CrossEvent {
40+
return (
41+
defined(value) &&
42+
typeof value === 'object' &&
43+
typeof value.query === 'string' &&
44+
typeof value.type === 'string' &&
45+
isCrossEventType(value.type)
46+
);
47+
}

static/app/views/explore/queryParams/readableQueryParams.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {Sort} from 'sentry/utils/discover/fields';
22
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
33
import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField';
4+
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
45
import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy';
56
import type {Mode} from 'sentry/views/explore/queryParams/mode';
67
import type {Visualize} from 'sentry/views/explore/queryParams/visualize';
@@ -16,6 +17,7 @@ export interface ReadableQueryParamsOptions {
1617
readonly mode: Mode;
1718
readonly query: string;
1819
readonly sortBys: Sort[];
20+
readonly crossEvents?: CrossEvent[];
1921
readonly id?: string;
2022
readonly title?: string;
2123
}
@@ -39,6 +41,8 @@ export class ReadableQueryParams {
3941
readonly id?: string;
4042
readonly title?: string;
4143

44+
readonly crossEvents?: CrossEvent[];
45+
4246
constructor(options: ReadableQueryParamsOptions) {
4347
this.extrapolate = options.extrapolate;
4448
this.mode = options.mode;
@@ -58,6 +62,8 @@ export class ReadableQueryParams {
5862

5963
this.id = options.id;
6064
this.title = options.title;
65+
66+
this.crossEvents = options.crossEvents;
6167
}
6268

6369
replace(options: Partial<ReadableQueryParamsOptions>) {
@@ -73,6 +79,7 @@ export class ReadableQueryParams {
7379
sortBys: options.sortBys ?? this.sortBys,
7480
id: options.id ?? this.id,
7581
title: options.title ?? this.title,
82+
crossEvents: options.crossEvents ?? this.crossEvents,
7683
});
7784
}
7885
}

static/app/views/explore/queryParams/writableQueryParams.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type {Sort} from 'sentry/utils/discover/fields';
22
import type {WritableAggregateField} from 'sentry/views/explore/queryParams/aggregateField';
3+
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
34
import type {Mode} from 'sentry/views/explore/queryParams/mode';
45

56
export interface WritableQueryParams {
67
aggregateCursor?: string | null;
78
aggregateFields?: readonly WritableAggregateField[] | null;
89
aggregateSortBys?: readonly Sort[] | null;
10+
crossEvents?: readonly CrossEvent[] | null;
911
cursor?: string | null;
1012
extrapolate?: boolean;
1113
fields?: string[] | null;

static/app/views/explore/spans/spansQueryParams.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {DEFAULT_VISUALIZATION} from 'sentry/views/explore/contexts/pageParamsCon
88
import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField';
99
import {getAggregateFieldsFromLocation} from 'sentry/views/explore/queryParams/aggregateField';
1010
import {getAggregateSortBysFromLocation} from 'sentry/views/explore/queryParams/aggregateSortBy';
11+
import {getCrossEventsFromLocation} from 'sentry/views/explore/queryParams/crossEvent';
1112
import {getCursorFromLocation} from 'sentry/views/explore/queryParams/cursor';
1213
import {getExtrapolateFromLocation} from 'sentry/views/explore/queryParams/extrapolate';
1314
import {getFieldsFromLocation} from 'sentry/views/explore/queryParams/field';
@@ -49,6 +50,7 @@ const SPANS_AGGREGATE_SORT_KEY = 'aggregateSort';
4950
const SPANS_EXTRAPOLATE_KEY = 'extrapolate';
5051
const SPANS_ID_KEY = ID_KEY;
5152
const SPANS_TITLE_KEY = TITLE_KEY;
53+
const SPANS_CROSS_EVENTS_KEY = 'crossEvents';
5254

5355
export function useSpansDataset(): DiscoverDatasets {
5456
return DiscoverDatasets.SPANS;
@@ -84,6 +86,8 @@ export function getReadableQueryParamsFromLocation(
8486
const id = getIdFromLocation(location, SPANS_ID_KEY);
8587
const title = getTitleFromLocation(location, SPANS_TITLE_KEY);
8688

89+
const crossEvents = getCrossEventsFromLocation(location, SPANS_CROSS_EVENTS_KEY);
90+
8791
return new ReadableQueryParams({
8892
extrapolate,
8993
mode,
@@ -99,6 +103,8 @@ export function getReadableQueryParamsFromLocation(
99103

100104
id,
101105
title,
106+
107+
crossEvents,
102108
});
103109
}
104110

@@ -148,6 +154,14 @@ export function getTargetWithReadableQueryParams(
148154
)
149155
);
150156

157+
updateNullableLocation(
158+
target,
159+
SPANS_CROSS_EVENTS_KEY,
160+
writableQueryParams?.crossEvents === null
161+
? null
162+
: JSON.stringify(writableQueryParams.crossEvents)
163+
);
164+
151165
return target;
152166
}
153167

static/app/views/explore/spans/spansTab.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import {Fragment, useCallback, useEffect, useMemo} from 'react';
1+
import {Fragment, useCallback, useEffect, useMemo, type Key} from 'react';
22
import {css} from '@emotion/react';
33
import styled from '@emotion/styled';
44

5+
import {Grid} from '@sentry/scraps/layout';
6+
57
import Feature from 'sentry/components/acl/feature';
68
import {Alert} from 'sentry/components/core/alert';
79
import {Button} from 'sentry/components/core/button';
10+
import {DropdownMenu, type DropdownMenuProps} from 'sentry/components/dropdownMenu';
811
import * as Layout from 'sentry/components/layouts/thirds';
912
import type {DatePageFilterProps} from 'sentry/components/organizations/datePageFilter';
1013
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
@@ -22,6 +25,7 @@ import {
2225
} from 'sentry/components/searchQueryBuilder/context';
2326
import {useCaseInsensitivity} from 'sentry/components/searchQueryBuilder/hooks';
2427
import {TourElement} from 'sentry/components/tours/components';
28+
import {IconAdd} from 'sentry/icons';
2529
import {IconChevron} from 'sentry/icons/iconChevron';
2630
import {t} from 'sentry/locale';
2731
import type {Organization} from 'sentry/types/organization';
@@ -48,7 +52,6 @@ import {
4852
ExploreBodySearch,
4953
ExploreContentSection,
5054
ExploreControlSection,
51-
ExploreFilterSection,
5255
ExploreSchemaHintsSection,
5356
} from 'sentry/views/explore/components/styles';
5457
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
@@ -62,15 +65,18 @@ import {useExploreTracesTable} from 'sentry/views/explore/hooks/useExploreTraces
6265
import {Tab, useTab} from 'sentry/views/explore/hooks/useTab';
6366
import {useVisitQuery} from 'sentry/views/explore/hooks/useVisitQuery';
6467
import {
68+
useQueryParamsCrossEvents,
6569
useQueryParamsExtrapolate,
6670
useQueryParamsFields,
6771
useQueryParamsId,
6872
useQueryParamsMode,
6973
useQueryParamsQuery,
7074
useQueryParamsVisualizes,
7175
useSetQueryParams,
76+
useSetQueryParamsCrossEvents,
7277
useSetQueryParamsVisualizes,
7378
} from 'sentry/views/explore/queryParams/context';
79+
import {isCrossEventType} from 'sentry/views/explore/queryParams/crossEvent';
7480
import {ExploreCharts} from 'sentry/views/explore/spans/charts';
7581
import {DroppedFieldsAlert} from 'sentry/views/explore/spans/droppedFieldsAlert';
7682
import {ExtrapolationEnabledAlert} from 'sentry/views/explore/spans/extrapolationEnabledAlert';
@@ -173,6 +179,40 @@ function useVisitExplore() {
173179
}, [id, visitQuery]);
174180
}
175181

182+
const crossEventDropdownItems: DropdownMenuProps['items'] = [
183+
{key: 'spans', label: t('Spans')},
184+
{key: 'logs', label: t('Logs')},
185+
{key: 'metrics', label: t('Metrics')},
186+
];
187+
188+
function CrossEventQueryingDropdown() {
189+
const crossEvents = useQueryParamsCrossEvents();
190+
const setCrossEvents = useSetQueryParamsCrossEvents();
191+
192+
const onAction = (key: Key) => {
193+
if (typeof key !== 'string' || !isCrossEventType(key)) {
194+
return;
195+
}
196+
197+
if (!crossEvents || crossEvents.length === 0) {
198+
setCrossEvents([{query: '', type: key}]);
199+
return;
200+
}
201+
};
202+
203+
return (
204+
<DropdownMenu
205+
triggerProps={{
206+
size: 'md',
207+
showChevron: false,
208+
icon: <IconAdd />,
209+
}}
210+
items={crossEventDropdownItems}
211+
onAction={onAction}
212+
/>
213+
);
214+
}
215+
176216
interface SpanTabSearchSectionProps {
177217
datePageFilterProps: DatePageFilterProps;
178218
}
@@ -201,6 +241,9 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
201241
const organization = useOrganization();
202242
const areAiFeaturesAllowed =
203243
!organization?.hideAiFeatures && organization.features.includes('gen-ai-features');
244+
const hasCrossEventQuerying = organization.features.includes(
245+
'traces-page-cross-event-querying'
246+
);
204247

205248
const {
206249
tags: numberTags,
@@ -300,7 +343,7 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
300343
position="bottom"
301344
margin={-8}
302345
>
303-
<ExploreFilterSection>
346+
<Grid gap="md" columns={{sm: '1fr', md: 'minmax(300px, auto) 1fr min-content'}}>
304347
<StyledPageFilterBar condensed>
305348
<ProjectPageFilter />
306349
<EnvironmentPageFilter />
@@ -309,7 +352,8 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
309352
<SpansSearchBar
310353
eapSpanSearchQueryBuilderProps={eapSpanSearchQueryBuilderProps}
311354
/>
312-
</ExploreFilterSection>
355+
{hasCrossEventQuerying ? <CrossEventQueryingDropdown /> : null}
356+
</Grid>
313357
<ExploreSchemaHintsSection>
314358
<SchemaHintsList
315359
supportedAggregates={

0 commit comments

Comments
 (0)