Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions static/app/views/explore/queryParams/context.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {useMemo, type ReactNode} from 'react';

import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary';

import {useResettableState} from 'sentry/utils/useResettableState';
import {
defaultAggregateFields,
defaultAggregateSortBys,
defaultFields,
defaultQuery,
defaultSortBys,
} from 'sentry/views/explore/metrics/metricQuery';
import {
QueryParamsContextProvider,
useQueryParamsCrossEvents,
useSetQueryParamsCrossEvents,
} from 'sentry/views/explore/queryParams/context';
import {defaultCursor} from 'sentry/views/explore/queryParams/cursor';
import {Mode} from 'sentry/views/explore/queryParams/mode';
import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams';

const mockSetQueryParams = jest.fn();

function Wrapper({children}: {children: ReactNode}) {
const [query] = useResettableState(defaultQuery);

const readableQueryParams = useMemo(
() =>
new ReadableQueryParams({
aggregateCursor: defaultCursor(),
aggregateFields: defaultAggregateFields(),
aggregateSortBys: defaultAggregateSortBys(defaultAggregateFields()),
cursor: defaultCursor(),
extrapolate: true,
fields: defaultFields(),
mode: Mode.AGGREGATE,
query,
sortBys: defaultSortBys(defaultFields()),
crossEvents: [{query: 'foo', type: 'spans'}],
}),
[query]
);

return (
<QueryParamsContextProvider
isUsingDefaultFields={false}
queryParams={readableQueryParams}
setQueryParams={mockSetQueryParams}
shouldManageFields={false}
>
{children}
</QueryParamsContextProvider>
);
}

describe('QueryParamsContext', () => {
describe('crossEvents', () => {
describe('useQueryParamsCrossEvents', () => {
it('should return the crossEvents', () => {
const {result} = renderHookWithProviders(() => useQueryParamsCrossEvents(), {
additionalWrapper: Wrapper,
});

expect(result.current).toEqual([{query: 'foo', type: 'spans'}]);
});
});

describe('useSetQueryParamsCrossEvents', () => {
it('should set the crossEvents', () => {
renderHookWithProviders(
() => {
const setCrossEvents = useSetQueryParamsCrossEvents();
setCrossEvents([{query: 'bar', type: 'logs'}]);
return useQueryParamsCrossEvents();
},
{additionalWrapper: Wrapper}
);

expect(mockSetQueryParams).toHaveBeenCalled();
expect(mockSetQueryParams).toHaveBeenCalledWith({
crossEvents: [{query: 'bar', type: 'logs'}],
});
});
});
});
});
17 changes: 17 additions & 0 deletions static/app/views/explore/queryParams/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
AggregateField,
WritableAggregateField,
} from 'sentry/views/explore/queryParams/aggregateField';
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy';
import {updateNullableLocation} from 'sentry/views/explore/queryParams/location';
import {deriveUpdatedManagedFields} from 'sentry/views/explore/queryParams/managedFields';
Expand Down Expand Up @@ -462,3 +463,19 @@ export function useSetQueryParamsSavedQuery() {
[location, navigate]
);
}

export function useQueryParamsCrossEvents() {
const queryParams = useQueryParams();
return queryParams.crossEvents;
}

export function useSetQueryParamsCrossEvents() {
const setQueryParams = useSetQueryParams();

return useCallback(
(crossEvents: CrossEvent[]) => {
setQueryParams({crossEvents});
},
[setQueryParams]
);
}
47 changes: 47 additions & 0 deletions static/app/views/explore/queryParams/crossEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {Location} from 'history';

import {defined} from 'sentry/utils';

type CrossEventType = 'logs' | 'spans' | 'metrics';

export interface CrossEvent {
query: string;
type: CrossEventType;
}

export function getCrossEventsFromLocation(
location: Location,
key: string
): CrossEvent[] | undefined {
let json: any;

if (!defined(location.query?.[key]) || Array.isArray(location.query?.[key])) {
return undefined;
}

try {
json = JSON.parse(location.query?.[key]);
} catch {
return undefined;
}

if (Array.isArray(json) && json.every(isCrossEvent)) {
return json;
}

return undefined;
}

export function isCrossEventType(value: string): value is CrossEventType {
return value === 'logs' || value === 'spans' || value === 'metrics';
}

function isCrossEvent(value: any): value is CrossEvent {
return (
defined(value) &&
typeof value === 'object' &&
typeof value.query === 'string' &&
typeof value.type === 'string' &&
isCrossEventType(value.type)
);
}
7 changes: 7 additions & 0 deletions static/app/views/explore/queryParams/readableQueryParams.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {Sort} from 'sentry/utils/discover/fields';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField';
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
import {isGroupBy} from 'sentry/views/explore/queryParams/groupBy';
import type {Mode} from 'sentry/views/explore/queryParams/mode';
import type {Visualize} from 'sentry/views/explore/queryParams/visualize';
Expand All @@ -16,6 +17,7 @@ export interface ReadableQueryParamsOptions {
readonly mode: Mode;
readonly query: string;
readonly sortBys: Sort[];
readonly crossEvents?: CrossEvent[];
readonly id?: string;
readonly title?: string;
}
Expand All @@ -39,6 +41,8 @@ export class ReadableQueryParams {
readonly id?: string;
readonly title?: string;

readonly crossEvents?: CrossEvent[];

constructor(options: ReadableQueryParamsOptions) {
this.extrapolate = options.extrapolate;
this.mode = options.mode;
Expand All @@ -58,6 +62,8 @@ export class ReadableQueryParams {

this.id = options.id;
this.title = options.title;

this.crossEvents = options.crossEvents;
}

replace(options: Partial<ReadableQueryParamsOptions>) {
Expand All @@ -73,6 +79,7 @@ export class ReadableQueryParams {
sortBys: options.sortBys ?? this.sortBys,
id: options.id ?? this.id,
title: options.title ?? this.title,
crossEvents: options.crossEvents ?? this.crossEvents,
});
}
}
2 changes: 2 additions & 0 deletions static/app/views/explore/queryParams/writableQueryParams.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {Sort} from 'sentry/utils/discover/fields';
import type {WritableAggregateField} from 'sentry/views/explore/queryParams/aggregateField';
import type {CrossEvent} from 'sentry/views/explore/queryParams/crossEvent';
import type {Mode} from 'sentry/views/explore/queryParams/mode';

export interface WritableQueryParams {
aggregateCursor?: string | null;
aggregateFields?: readonly WritableAggregateField[] | null;
aggregateSortBys?: readonly Sort[] | null;
crossEvents?: readonly CrossEvent[] | null;
cursor?: string | null;
extrapolate?: boolean;
fields?: string[] | null;
Expand Down
14 changes: 14 additions & 0 deletions static/app/views/explore/spans/spansQueryParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {DEFAULT_VISUALIZATION} from 'sentry/views/explore/contexts/pageParamsCon
import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField';
import {getAggregateFieldsFromLocation} from 'sentry/views/explore/queryParams/aggregateField';
import {getAggregateSortBysFromLocation} from 'sentry/views/explore/queryParams/aggregateSortBy';
import {getCrossEventsFromLocation} from 'sentry/views/explore/queryParams/crossEvent';
import {getCursorFromLocation} from 'sentry/views/explore/queryParams/cursor';
import {getExtrapolateFromLocation} from 'sentry/views/explore/queryParams/extrapolate';
import {getFieldsFromLocation} from 'sentry/views/explore/queryParams/field';
Expand Down Expand Up @@ -49,6 +50,7 @@ const SPANS_AGGREGATE_SORT_KEY = 'aggregateSort';
const SPANS_EXTRAPOLATE_KEY = 'extrapolate';
const SPANS_ID_KEY = ID_KEY;
const SPANS_TITLE_KEY = TITLE_KEY;
const SPANS_CROSS_EVENTS_KEY = 'crossEvents';

export function useSpansDataset(): DiscoverDatasets {
return DiscoverDatasets.SPANS;
Expand Down Expand Up @@ -84,6 +86,8 @@ export function getReadableQueryParamsFromLocation(
const id = getIdFromLocation(location, SPANS_ID_KEY);
const title = getTitleFromLocation(location, SPANS_TITLE_KEY);

const crossEvents = getCrossEventsFromLocation(location, SPANS_CROSS_EVENTS_KEY);

return new ReadableQueryParams({
extrapolate,
mode,
Expand All @@ -99,6 +103,8 @@ export function getReadableQueryParamsFromLocation(

id,
title,

crossEvents,
});
}

Expand Down Expand Up @@ -148,6 +154,14 @@ export function getTargetWithReadableQueryParams(
)
);

updateNullableLocation(
target,
SPANS_CROSS_EVENTS_KEY,
writableQueryParams?.crossEvents === null
? null
: JSON.stringify(writableQueryParams.crossEvents)
);

return target;
}

Expand Down
52 changes: 48 additions & 4 deletions static/app/views/explore/spans/spansTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {Fragment, useCallback, useEffect, useMemo} from 'react';
import {Fragment, useCallback, useEffect, useMemo, type Key} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';

import {Grid} from '@sentry/scraps/layout';

import Feature from 'sentry/components/acl/feature';
import {Alert} from 'sentry/components/core/alert';
import {Button} from 'sentry/components/core/button';
import {DropdownMenu, type DropdownMenuProps} from 'sentry/components/dropdownMenu';
import * as Layout from 'sentry/components/layouts/thirds';
import type {DatePageFilterProps} from 'sentry/components/organizations/datePageFilter';
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
Expand All @@ -22,6 +25,7 @@ import {
} from 'sentry/components/searchQueryBuilder/context';
import {useCaseInsensitivity} from 'sentry/components/searchQueryBuilder/hooks';
import {TourElement} from 'sentry/components/tours/components';
import {IconAdd} from 'sentry/icons';
import {IconChevron} from 'sentry/icons/iconChevron';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types/organization';
Expand All @@ -48,7 +52,6 @@ import {
ExploreBodySearch,
ExploreContentSection,
ExploreControlSection,
ExploreFilterSection,
ExploreSchemaHintsSection,
} from 'sentry/views/explore/components/styles';
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
Expand All @@ -62,15 +65,18 @@ import {useExploreTracesTable} from 'sentry/views/explore/hooks/useExploreTraces
import {Tab, useTab} from 'sentry/views/explore/hooks/useTab';
import {useVisitQuery} from 'sentry/views/explore/hooks/useVisitQuery';
import {
useQueryParamsCrossEvents,
useQueryParamsExtrapolate,
useQueryParamsFields,
useQueryParamsId,
useQueryParamsMode,
useQueryParamsQuery,
useQueryParamsVisualizes,
useSetQueryParams,
useSetQueryParamsCrossEvents,
useSetQueryParamsVisualizes,
} from 'sentry/views/explore/queryParams/context';
import {isCrossEventType} from 'sentry/views/explore/queryParams/crossEvent';
import {ExploreCharts} from 'sentry/views/explore/spans/charts';
import {DroppedFieldsAlert} from 'sentry/views/explore/spans/droppedFieldsAlert';
import {ExtrapolationEnabledAlert} from 'sentry/views/explore/spans/extrapolationEnabledAlert';
Expand Down Expand Up @@ -173,6 +179,40 @@ function useVisitExplore() {
}, [id, visitQuery]);
}

const crossEventDropdownItems: DropdownMenuProps['items'] = [
{key: 'spans', label: t('Spans')},
{key: 'logs', label: t('Logs')},
{key: 'metrics', label: t('Metrics')},
];

function CrossEventQueryingDropdown() {
const crossEvents = useQueryParamsCrossEvents();
const setCrossEvents = useSetQueryParamsCrossEvents();

const onAction = (key: Key) => {
if (typeof key !== 'string' || !isCrossEventType(key)) {
return;
}

if (!crossEvents || crossEvents.length === 0) {
setCrossEvents([{query: '', type: key}]);
return;
}
};

return (
<DropdownMenu
triggerProps={{
size: 'md',
showChevron: false,
icon: <IconAdd />,
}}
items={crossEventDropdownItems}
onAction={onAction}
/>
);
}

interface SpanTabSearchSectionProps {
datePageFilterProps: DatePageFilterProps;
}
Expand Down Expand Up @@ -201,6 +241,9 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
const organization = useOrganization();
const areAiFeaturesAllowed =
!organization?.hideAiFeatures && organization.features.includes('gen-ai-features');
const hasCrossEventQuerying = organization.features.includes(
'traces-page-cross-event-querying'
);

const {
tags: numberTags,
Expand Down Expand Up @@ -300,7 +343,7 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
position="bottom"
margin={-8}
>
<ExploreFilterSection>
<Grid gap="md" columns={{sm: '1fr', md: 'minmax(300px, auto) 1fr min-content'}}>
<StyledPageFilterBar condensed>
<ProjectPageFilter />
<EnvironmentPageFilter />
Expand All @@ -309,7 +352,8 @@ function SpanTabSearchSection({datePageFilterProps}: SpanTabSearchSectionProps)
<SpansSearchBar
eapSpanSearchQueryBuilderProps={eapSpanSearchQueryBuilderProps}
/>
</ExploreFilterSection>
{hasCrossEventQuerying ? <CrossEventQueryingDropdown /> : null}
</Grid>
<ExploreSchemaHintsSection>
<SchemaHintsList
supportedAggregates={
Expand Down
Loading