Skip to content

Commit 3cb4a59

Browse files
authored
feat(ourlogs): Add save query for logs (#97450)
### Summary This adds "save query" action item for logs in the "save as" on explore, and adds 'logs' type when ourlogs-enabled flag is set on the "All Queries" saved table view. #### Screenshots |<img width="825" height="340" alt="Screenshot 2025-08-07 at 7 29 35 PM" src="https://github.com/user-attachments/assets/9cb2f004-7788-445d-9e0a-70c18417b27e" />|<img width="1075" height="276" alt="Screenshot 2025-08-07 at 7 29 16 PM" src="https://github.com/user-attachments/assets/00bb87e7-fbd5-48b3-bb06-58f6d3e420c5" />| |-|-|
1 parent 8e50cfb commit 3cb4a59

File tree

17 files changed

+1138
-325
lines changed

17 files changed

+1138
-325
lines changed

static/app/components/modals/explore/saveQueryModal.spec.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrar
33

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

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

@@ -24,6 +25,7 @@ describe('SaveQueryModal', function () {
2425
closeModal={() => {}}
2526
organization={initialData.organization}
2627
saveQuery={saveQuery}
28+
traceItemDataset={TraceItemDataset.SPANS}
2729
/>
2830
);
2931

@@ -45,6 +47,7 @@ describe('SaveQueryModal', function () {
4547
closeModal={() => {}}
4648
organization={initialData.organization}
4749
saveQuery={saveQuery}
50+
traceItemDataset={TraceItemDataset.SPANS}
4851
/>
4952
);
5053

@@ -69,6 +72,7 @@ describe('SaveQueryModal', function () {
6972
closeModal={() => {}}
7073
organization={initialData.organization}
7174
saveQuery={saveQuery}
75+
traceItemDataset={TraceItemDataset.SPANS}
7276
/>
7377
);
7478

@@ -95,6 +99,28 @@ describe('SaveQueryModal', function () {
9599
organization={initialData.organization}
96100
saveQuery={saveQuery}
97101
name="Initial Query Name"
102+
traceItemDataset={TraceItemDataset.SPANS}
103+
/>
104+
);
105+
106+
expect(screen.getByRole('textbox')).toHaveValue('Initial Query Name');
107+
expect(screen.getByText('Rename Query')).toBeInTheDocument();
108+
expect(screen.getByText('Save Changes')).toBeInTheDocument();
109+
});
110+
111+
it('should render ui with logs dataset', function () {
112+
const saveQuery = jest.fn();
113+
render(
114+
<SaveQueryModal
115+
Header={stubEl}
116+
Footer={stubEl as ModalRenderProps['Footer']}
117+
Body={stubEl as ModalRenderProps['Body']}
118+
CloseButton={stubEl}
119+
closeModal={() => {}}
120+
organization={initialData.organization}
121+
saveQuery={saveQuery}
122+
name="Initial Query Name"
123+
traceItemDataset={TraceItemDataset.LOGS}
98124
/>
99125
);
100126

static/app/components/modals/explore/saveQueryModal.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import type {Organization, SavedQuery} from 'sentry/types/organization';
1818
import {defined} from 'sentry/utils';
1919
import {trackAnalytics} from 'sentry/utils/analytics';
2020
import useOrganization from 'sentry/utils/useOrganization';
21+
import {useSetLogsSavedQueryInfo} from 'sentry/views/explore/contexts/logs/logsPageParams';
2122
import {useSetExplorePageParams} from 'sentry/views/explore/contexts/pageParamsContext';
23+
import {TraceItemDataset} from 'sentry/views/explore/types';
2224

2325
export type SaveQueryModalProps = {
2426
organization: Organization;
2527
saveQuery: (name: string, starred?: boolean) => Promise<SavedQuery>;
28+
traceItemDataset: TraceItemDataset;
2629
name?: string;
2730
source?: 'toolbar' | 'table';
2831
};
@@ -37,6 +40,7 @@ function SaveQueryModal({
3740
saveQuery,
3841
name: initialName,
3942
source,
43+
traceItemDataset,
4044
}: Props) {
4145
const organization = useOrganization();
4246

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

4751
const setExplorePageParams = useSetExplorePageParams();
52+
const setLogsQuery = useSetLogsSavedQueryInfo();
4853

4954
const updatePageIdAndTitle = useCallback(
5055
(id: string, title: string) => {
51-
setExplorePageParams({id, title});
56+
if (traceItemDataset === TraceItemDataset.LOGS) {
57+
setLogsQuery(id, title);
58+
} else if (traceItemDataset === TraceItemDataset.SPANS) {
59+
setExplorePageParams({id, title});
60+
}
5261
},
53-
[setExplorePageParams]
62+
[setExplorePageParams, setLogsQuery, traceItemDataset]
5463
);
5564

5665
const onSave = useCallback(async () => {
@@ -63,12 +72,21 @@ function SaveQueryModal({
6372
}
6473
addSuccessMessage(t('Query saved successfully'));
6574
if (defined(source)) {
66-
trackAnalytics('trace_explorer.save_query_modal', {
67-
action: 'submit',
68-
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
69-
ui_source: source,
70-
organization,
71-
});
75+
if (traceItemDataset === TraceItemDataset.LOGS) {
76+
trackAnalytics('logs.save_query_modal', {
77+
action: 'submit',
78+
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
79+
ui_source: source,
80+
organization,
81+
});
82+
} else if (traceItemDataset === TraceItemDataset.SPANS) {
83+
trackAnalytics('trace_explorer.save_query_modal', {
84+
action: 'submit',
85+
save_type: initialName === undefined ? 'save_new_query' : 'rename_query',
86+
ui_source: source,
87+
organization,
88+
});
89+
}
7290
}
7391
closeModal();
7492
} catch (error) {
@@ -86,6 +104,7 @@ function SaveQueryModal({
86104
organization,
87105
initialName,
88106
source,
107+
traceItemDataset,
89108
]);
90109

91110
return (

static/app/utils/analytics/logsAnalyticsEvent.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ export type LogsAnalyticsEventParameters = {
3535
'logs.issue_details.drawer_opened': {
3636
organization: Organization;
3737
};
38+
'logs.save_as': {
39+
save_type: 'alert' | 'dashboard' | 'update_query';
40+
ui_source: 'toolbar' | 'chart' | 'compare chart' | 'searchbar';
41+
};
42+
'logs.save_query_modal': {
43+
action: 'open' | 'submit';
44+
save_type: 'save_new_query' | 'rename_query';
45+
ui_source: 'toolbar' | 'table';
46+
};
3847
'logs.table.row_expanded': {
3948
log_id: string;
4049
page_source: LogsAnalyticsPageSource;
@@ -50,4 +59,6 @@ export const logsAnalyticsEventMap: Record<LogsAnalyticsEventKey, string | null>
5059
'logs.explorer.metadata': 'Log Explorer Pageload Metadata',
5160
'logs.issue_details.drawer_opened': 'Issues Page Logs Drawer Opened',
5261
'logs.table.row_expanded': 'Expanded Log Row Details',
62+
'logs.save_as': 'Logs Save As',
63+
'logs.save_query_modal': 'Logs Save Query Modal',
5364
};

static/app/views/explore/contexts/logs/logsPageParams.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,26 @@ interface LogsPageParams {
8989
*/
9090
readonly groupBy?: string;
9191

92+
/**
93+
* The id of the query, if a saved query.
94+
*/
95+
readonly id?: string;
9296
/**
9397
* If provided, add a 'trace:{trace id}' to all queries.
9498
* Used in embedded views like error page and trace page.
9599
* Can be an array of trace IDs on some pages (eg. replays)
96100
*/
97101
readonly limitToTraceId?: string | string[];
102+
98103
/**
99104
* If provided, ignores the project in the location and uses the provided project IDs.
100105
* Useful for cross-project traces when project is in the location.
101106
*/
102107
readonly projectIds?: number[];
108+
/**
109+
* The title of the query, if a saved query.
110+
*/
111+
readonly title?: string;
103112
}
104113

105114
type NullablePartial<T> = {
@@ -223,7 +232,7 @@ export function LogsPageParamsProvider({
223232
);
224233
}
225234

226-
const useLogsPageParams = _useLogsPageParams;
235+
export const useLogsPageParams = _useLogsPageParams;
227236

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

452+
export function useSetLogsSavedQueryInfo() {
453+
const setPageParams = useSetLogsPageParams();
454+
return useCallback(
455+
(id: string, title: string) => {
456+
setPageParams({id, title});
457+
},
458+
[setPageParams]
459+
);
460+
}
461+
443462
interface ToggleableSortBy {
444463
field: string;
445464
defaultDirection?: 'asc' | 'desc'; // Defaults to descending if not provided.

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

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import {useCallback, useMemo} from 'react';
22

3+
import type {DateString} from 'sentry/types/core';
34
import type {User} from 'sentry/types/user';
45
import {defined} from 'sentry/utils';
56
import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
67
import useOrganization from 'sentry/utils/useOrganization';
78
import type {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
9+
import {TraceItemDataset} from 'sentry/views/explore/types';
810

911
export type RawGroupBy = {
1012
groupBy: string;
@@ -41,7 +43,8 @@ type ReadableQuery = {
4143
visualize?: RawVisualize[];
4244
};
4345

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

8790
// Comes from ExploreSavedQueryModelSerializer
8891
type ReadableSavedQuery = {
92+
dataset: 'logs' | 'spans' | 'segment_spans'; // ExploreSavedQueryDataset
8993
dateAdded: string;
9094
dateUpdated: string;
9195
id: number;
@@ -95,7 +99,6 @@ type ReadableSavedQuery = {
9599
position: number | null;
96100
projects: number[];
97101
query: [ReadableQuery, ...ReadableQuery[]];
98-
queryDataset: string;
99102
starred: boolean;
100103
createdBy?: User;
101104
end?: string;
@@ -114,15 +117,15 @@ export class SavedQuery {
114117
name: string;
115118
position: number | null;
116119
projects: number[];
117-
query: [Query, ...Query[]];
118-
queryDataset: string;
120+
query: [SavedQueryQuery, ...SavedQueryQuery[]];
121+
dataset: ReadableSavedQuery['dataset'];
119122
starred: boolean;
120123
createdBy?: User;
121-
end?: string;
124+
end?: string | DateString;
122125
environment?: string[];
123126
isPrebuilt?: boolean;
124127
range?: string;
125-
start?: string;
128+
start?: string | DateString;
126129

127130
constructor(savedQuery: ReadableSavedQuery) {
128131
this.dateAdded = savedQuery.dateAdded;
@@ -134,20 +137,24 @@ export class SavedQuery {
134137
this.position = savedQuery.position;
135138
this.projects = savedQuery.projects;
136139
this.query = [
137-
new Query(savedQuery.query[0]),
138-
...savedQuery.query.slice(1).map(q => new Query(q)),
140+
new SavedQueryQuery(savedQuery.query[0]),
141+
...savedQuery.query.slice(1).map(q => new SavedQueryQuery(q)),
139142
];
140-
this.queryDataset = savedQuery.queryDataset;
141143
this.starred = savedQuery.starred;
142144
this.createdBy = savedQuery.createdBy;
143145
this.end = savedQuery.end;
144146
this.environment = savedQuery.environment;
145147
this.isPrebuilt = savedQuery.isPrebuilt;
146148
this.range = savedQuery.range;
147149
this.start = savedQuery.start;
150+
this.dataset = savedQuery.dataset;
148151
}
149152
}
150153

154+
export function getSavedQueryTraceItemDataset(dataset: ReadableSavedQuery['dataset']) {
155+
return DATASET_TO_TRACE_ITEM_DATASET_MAP[dataset];
156+
}
157+
151158
type Props = {
152159
cursor?: string;
153160
exclude?: 'owned' | 'shared';
@@ -226,3 +233,22 @@ export function useInvalidateSavedQuery(id?: string) {
226233
});
227234
}, [queryClient, organization.slug, id]);
228235
}
236+
237+
const DATASET_LABEL_MAP: Record<ReadableSavedQuery['dataset'], string> = {
238+
logs: 'Logs',
239+
spans: 'Traces',
240+
segment_spans: 'Traces',
241+
};
242+
243+
const DATASET_TO_TRACE_ITEM_DATASET_MAP: Record<
244+
ReadableSavedQuery['dataset'],
245+
TraceItemDataset
246+
> = {
247+
logs: TraceItemDataset.LOGS,
248+
spans: TraceItemDataset.SPANS,
249+
segment_spans: TraceItemDataset.SPANS,
250+
};
251+
252+
export function getSavedQueryDatasetLabel(dataset: ReadableSavedQuery['dataset']) {
253+
return DATASET_LABEL_MAP[dataset];
254+
}

0 commit comments

Comments
 (0)