Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions static/app/components/modals/explore/saveQueryModal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar

import type {ModalRenderProps} from 'sentry/actionCreators/modal';
import SaveQueryModal from 'sentry/components/modals/explore/saveQueryModal';
import {TraceItemDataset} from 'sentry/views/explore/types';

const stubEl = (props: {children?: React.ReactNode}) => <div>{props.children}</div>;

Expand All @@ -24,6 +25,7 @@ describe('SaveQueryModal', function () {
closeModal={() => {}}
organization={initialData.organization}
saveQuery={saveQuery}
traceItemDataset={TraceItemDataset.SPANS}
/>
);

Expand All @@ -45,6 +47,7 @@ describe('SaveQueryModal', function () {
closeModal={() => {}}
organization={initialData.organization}
saveQuery={saveQuery}
traceItemDataset={TraceItemDataset.SPANS}
/>
);

Expand All @@ -69,6 +72,7 @@ describe('SaveQueryModal', function () {
closeModal={() => {}}
organization={initialData.organization}
saveQuery={saveQuery}
traceItemDataset={TraceItemDataset.SPANS}
/>
);

Expand All @@ -95,6 +99,28 @@ describe('SaveQueryModal', function () {
organization={initialData.organization}
saveQuery={saveQuery}
name="Initial Query Name"
traceItemDataset={TraceItemDataset.SPANS}
/>
);

expect(screen.getByRole('textbox')).toHaveValue('Initial Query Name');
expect(screen.getByText('Rename Query')).toBeInTheDocument();
expect(screen.getByText('Save Changes')).toBeInTheDocument();
});

it('should render ui with logs dataset', function () {
const saveQuery = jest.fn();
render(
<SaveQueryModal
Header={stubEl}
Footer={stubEl as ModalRenderProps['Footer']}
Body={stubEl as ModalRenderProps['Body']}
CloseButton={stubEl}
closeModal={() => {}}
organization={initialData.organization}
saveQuery={saveQuery}
name="Initial Query Name"
traceItemDataset={TraceItemDataset.LOGS}
/>
);

Expand Down
35 changes: 27 additions & 8 deletions static/app/components/modals/explore/saveQueryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import type {Organization, SavedQuery} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import useOrganization from 'sentry/utils/useOrganization';
import {useSetLogsSavedQueryInfo} from 'sentry/views/explore/contexts/logs/logsPageParams';
import {useSetExplorePageParams} from 'sentry/views/explore/contexts/pageParamsContext';
import {TraceItemDataset} from 'sentry/views/explore/types';

export type SaveQueryModalProps = {
organization: Organization;
saveQuery: (name: string, starred?: boolean) => Promise<SavedQuery>;
traceItemDataset: TraceItemDataset;
name?: string;
source?: 'toolbar' | 'table';
};
Expand All @@ -37,6 +40,7 @@ function SaveQueryModal({
saveQuery,
name: initialName,
source,
traceItemDataset,
}: Props) {
const organization = useOrganization();

Expand All @@ -45,12 +49,17 @@ function SaveQueryModal({
const [starred, setStarred] = useState(true);

const setExplorePageParams = useSetExplorePageParams();
const setLogsQuery = useSetLogsSavedQueryInfo();

const updatePageIdAndTitle = useCallback(
(id: string, title: string) => {
setExplorePageParams({id, title});
if (traceItemDataset === TraceItemDataset.LOGS) {
setLogsQuery(id, title);
} else if (traceItemDataset === TraceItemDataset.SPANS) {
setExplorePageParams({id, title});
}
},
[setExplorePageParams]
[setExplorePageParams, setLogsQuery, traceItemDataset]
);

const onSave = useCallback(async () => {
Expand All @@ -63,12 +72,21 @@ function SaveQueryModal({
}
addSuccessMessage(t('Query saved successfully'));
if (defined(source)) {
trackAnalytics('trace_explorer.save_query_modal', {
action: 'submit',
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
ui_source: source,
organization,
});
if (traceItemDataset === TraceItemDataset.LOGS) {
trackAnalytics('logs.save_query_modal', {
action: 'submit',
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
ui_source: source,
organization,
});
} else if (traceItemDataset === TraceItemDataset.SPANS) {
trackAnalytics('trace_explorer.save_query_modal', {
action: 'submit',
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
ui_source: source,
organization,
});
}
}
closeModal();
} catch (error) {
Expand All @@ -86,6 +104,7 @@ function SaveQueryModal({
organization,
initialName,
source,
traceItemDataset,
]);

return (
Expand Down
11 changes: 11 additions & 0 deletions static/app/utils/analytics/logsAnalyticsEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export type LogsAnalyticsEventParameters = {
'logs.issue_details.drawer_opened': {
organization: Organization;
};
'logs.save_as': {
save_type: 'alert' | 'dashboard' | 'update_query';
ui_source: 'toolbar' | 'chart' | 'compare chart' | 'searchbar';
};
'logs.save_query_modal': {
action: 'open' | 'submit';
save_type: 'save_new_query' | 'rename_query';
ui_source: 'toolbar' | 'table';
};
'logs.table.row_expanded': {
log_id: string;
page_source: LogsAnalyticsPageSource;
Expand All @@ -50,4 +59,6 @@ export const logsAnalyticsEventMap: Record<LogsAnalyticsEventKey, string | null>
'logs.explorer.metadata': 'Log Explorer Pageload Metadata',
'logs.issue_details.drawer_opened': 'Issues Page Logs Drawer Opened',
'logs.table.row_expanded': 'Expanded Log Row Details',
'logs.save_as': 'Logs Save As',
'logs.save_query_modal': 'Logs Save Query Modal',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing Analytics Event Definitions

Analytics events logs.star_query and logs.delete_query are tracked in starSavedQueryButton.tsx, savedQueryEditMenu.tsx, and savedQueriesTable.tsx but lack definitions in the LogsAnalyticsEventParameters type and logsAnalyticsEventMap in logsAnalyticsEvent.tsx. This causes TypeScript compilation errors and prevents proper analytics tracking.

Additional Locations (2)
Fix in Cursor Fix in Web

};
21 changes: 20 additions & 1 deletion static/app/views/explore/contexts/logs/logsPageParams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,26 @@ interface LogsPageParams {
*/
readonly groupBy?: string;

/**
* The id of the query, if a saved query.
*/
readonly id?: string;
/**
* If provided, add a 'trace:{trace id}' to all queries.
* Used in embedded views like error page and trace page.
* Can be an array of trace IDs on some pages (eg. replays)
*/
readonly limitToTraceId?: string | string[];

/**
* If provided, ignores the project in the location and uses the provided project IDs.
* Useful for cross-project traces when project is in the location.
*/
readonly projectIds?: number[];
/**
* The title of the query, if a saved query.
*/
readonly title?: string;
}

type NullablePartial<T> = {
Expand Down Expand Up @@ -223,7 +232,7 @@ export function LogsPageParamsProvider({
);
}

const useLogsPageParams = _useLogsPageParams;
export const useLogsPageParams = _useLogsPageParams;

const decodeLogsQuery = (location: Location): string => {
if (!location.query?.[LOGS_QUERY_KEY]) {
Expand Down Expand Up @@ -440,6 +449,16 @@ export function useSetLogsFields() {
);
}

export function useSetLogsSavedQueryInfo() {
const setPageParams = useSetLogsPageParams();
return useCallback(
(id: string, title: string) => {
setPageParams({id, title});
},
[setPageParams]
);
}

interface ToggleableSortBy {
field: string;
defaultDirection?: 'asc' | 'desc'; // Defaults to descending if not provided.
Expand Down
44 changes: 35 additions & 9 deletions static/app/views/explore/hooks/useGetSavedQueries.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useCallback, useMemo} from 'react';

import type {DateString} from 'sentry/types/core';
import type {User} from 'sentry/types/user';
import {defined} from 'sentry/utils';
import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';
import type {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
import {TraceItemDataset} from 'sentry/views/explore/types';

export type RawGroupBy = {
groupBy: string;
Expand Down Expand Up @@ -41,7 +43,8 @@ type ReadableQuery = {
visualize?: RawVisualize[];
};

class Query {
// This is the `query` property on our SavedQuery, which indicates the actualy query portion of the saved query, hence SavedQueryQuery.
export class SavedQueryQuery {
fields: string[];
mode: Mode;
orderby: string;
Expand Down Expand Up @@ -86,6 +89,7 @@ export type SortOption =

// Comes from ExploreSavedQueryModelSerializer
type ReadableSavedQuery = {
dataset: 'logs' | 'spans' | 'segment_spans'; // ExploreSavedQueryDataset
dateAdded: string;
dateUpdated: string;
id: number;
Expand All @@ -95,7 +99,6 @@ type ReadableSavedQuery = {
position: number | null;
projects: number[];
query: [ReadableQuery, ...ReadableQuery[]];
queryDataset: string;
starred: boolean;
createdBy?: User;
end?: string;
Expand All @@ -114,15 +117,15 @@ export class SavedQuery {
name: string;
position: number | null;
projects: number[];
query: [Query, ...Query[]];
queryDataset: string;
query: [SavedQueryQuery, ...SavedQueryQuery[]];
dataset: ReadableSavedQuery['dataset'];
starred: boolean;
createdBy?: User;
end?: string;
end?: string | DateString;
environment?: string[];
isPrebuilt?: boolean;
range?: string;
start?: string;
start?: string | DateString;

constructor(savedQuery: ReadableSavedQuery) {
this.dateAdded = savedQuery.dateAdded;
Expand All @@ -134,20 +137,24 @@ export class SavedQuery {
this.position = savedQuery.position;
this.projects = savedQuery.projects;
this.query = [
new Query(savedQuery.query[0]),
...savedQuery.query.slice(1).map(q => new Query(q)),
new SavedQueryQuery(savedQuery.query[0]),
...savedQuery.query.slice(1).map(q => new SavedQueryQuery(q)),
];
this.queryDataset = savedQuery.queryDataset;
this.starred = savedQuery.starred;
this.createdBy = savedQuery.createdBy;
this.end = savedQuery.end;
this.environment = savedQuery.environment;
this.isPrebuilt = savedQuery.isPrebuilt;
this.range = savedQuery.range;
this.start = savedQuery.start;
this.dataset = savedQuery.dataset;
}
}

export function getSavedQueryTraceItemDataset(dataset: ReadableSavedQuery['dataset']) {
return DATASET_TO_TRACE_ITEM_DATASET_MAP[dataset];
}

type Props = {
cursor?: string;
exclude?: 'owned' | 'shared';
Expand Down Expand Up @@ -226,3 +233,22 @@ export function useInvalidateSavedQuery(id?: string) {
});
}, [queryClient, organization.slug, id]);
}

const DATASET_LABEL_MAP: Record<ReadableSavedQuery['dataset'], string> = {
logs: 'Logs',
spans: 'Traces',
segment_spans: 'Traces',
};

const DATASET_TO_TRACE_ITEM_DATASET_MAP: Record<
ReadableSavedQuery['dataset'],
TraceItemDataset
> = {
logs: TraceItemDataset.LOGS,
spans: TraceItemDataset.SPANS,
segment_spans: TraceItemDataset.SPANS,
};

export function getSavedQueryDatasetLabel(dataset: ReadableSavedQuery['dataset']) {
return DATASET_LABEL_MAP[dataset];
}
Loading
Loading