Skip to content

Commit a1d6489

Browse files
feat(logs): Wire Seer AI visualization params into logs explore
Extract reusable helpers (getLogsSeerLocationQuery, getLogsSeerAggregateFields) from the LogsTabSeerComboBox component so the Seer-to-URL wiring is testable and decoupled from the React component. Add support for Seer returning visualization payloads (chart type + y-axes) and propagate them into the aggregate field and sort query params. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9c77e25 commit a1d6489

2 files changed

Lines changed: 342 additions & 112 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type {Location} from 'history';
2+
3+
import {
4+
getLogsSeerLocationQuery,
5+
type AskSeerSearchQuery,
6+
} from 'sentry/views/explore/logs/logsTabSeerComboBox';
7+
import {Mode} from 'sentry/views/explore/queryParams/mode';
8+
import {VisualizeFunction} from 'sentry/views/explore/queryParams/visualize';
9+
import {ChartType} from 'sentry/views/insights/common/components/chart';
10+
11+
const pageDatetime = {
12+
start: null,
13+
end: null,
14+
period: '7d',
15+
utc: null,
16+
};
17+
18+
function seerResult(overrides: Partial<AskSeerSearchQuery>): AskSeerSearchQuery {
19+
return {
20+
query: '',
21+
sort: '',
22+
groupBys: [],
23+
statsPeriod: '',
24+
start: null,
25+
end: null,
26+
mode: 'logs',
27+
visualizations: [],
28+
...overrides,
29+
};
30+
}
31+
32+
function aggregateFields(query: Location['query']) {
33+
const rawFields = query.aggregateField;
34+
const fields = Array.isArray(rawFields) ? rawFields : rawFields ? [rawFields] : [];
35+
return fields.map(field => JSON.parse(String(field)));
36+
}
37+
38+
describe('getLogsSeerLocationQuery', () => {
39+
it('applies raw logs response without aggregate visualization params', () => {
40+
const {query} = getLogsSeerLocationQuery({
41+
currentLocationQuery: {},
42+
currentAggregateFields: [new VisualizeFunction('count(message)')],
43+
pageDatetime,
44+
result: seerResult({
45+
query: 'severity:error',
46+
sort: '-timestamp',
47+
statsPeriod: '24h',
48+
visualizations: [{chartType: ChartType.LINE, yAxes: ['count(message)']}],
49+
}),
50+
});
51+
52+
expect(query).toMatchObject({
53+
logsQuery: 'severity:error',
54+
logsSortBys: ['-timestamp'],
55+
mode: Mode.SAMPLES,
56+
statsPeriod: '24h',
57+
});
58+
expect(query.aggregateField).toBeUndefined();
59+
expect(query.logsAggregateSortBys).toBeUndefined();
60+
});
61+
62+
it('applies aggregate count response with aggregate sort', () => {
63+
const {query} = getLogsSeerLocationQuery({
64+
currentLocationQuery: {},
65+
currentAggregateFields: [new VisualizeFunction('count(message)')],
66+
pageDatetime,
67+
result: seerResult({
68+
mode: 'aggregates',
69+
sort: '-count(message)',
70+
visualizations: [{chartType: ChartType.BAR, yAxes: ['count(message)']}],
71+
}),
72+
});
73+
74+
expect(query.mode).toBe(Mode.AGGREGATE);
75+
expect(query.logsAggregateSortBys).toEqual(['-count(message)']);
76+
expect(query.logsSortBys).toBeUndefined();
77+
expect(aggregateFields(query)).toEqual([
78+
{chartType: ChartType.BAR, yAxes: ['count(message)']},
79+
]);
80+
});
81+
82+
it('applies aggregate payload percentile visualization and sort', () => {
83+
const {query} = getLogsSeerLocationQuery({
84+
currentLocationQuery: {},
85+
currentAggregateFields: [new VisualizeFunction('count(message)')],
86+
pageDatetime,
87+
result: seerResult({
88+
mode: 'aggregates',
89+
sort: '-p90(payload_size)',
90+
visualizations: [{chartType: ChartType.LINE, yAxes: ['p90(payload_size)']}],
91+
}),
92+
});
93+
94+
expect(query.mode).toBe(Mode.AGGREGATE);
95+
expect(query.logsAggregateSortBys).toEqual(['-p90(payload_size)']);
96+
expect(aggregateFields(query)).toEqual([
97+
{chartType: ChartType.LINE, yAxes: ['p90(payload_size)']},
98+
]);
99+
});
100+
101+
it('orders grouped aggregate fields before returned visualizations', () => {
102+
const {query} = getLogsSeerLocationQuery({
103+
currentLocationQuery: {},
104+
currentAggregateFields: [
105+
new VisualizeFunction('count(message)'),
106+
{groupBy: 'severity'},
107+
],
108+
pageDatetime,
109+
result: seerResult({
110+
mode: 'aggregates',
111+
groupBys: ['service.name', 'environment'],
112+
sort: '-p95(payload_size)',
113+
visualizations: [{chartType: ChartType.AREA, yAxes: ['p95(payload_size)']}],
114+
}),
115+
});
116+
117+
expect(aggregateFields(query)).toEqual([
118+
{groupBy: 'service.name'},
119+
{groupBy: 'environment'},
120+
{chartType: ChartType.AREA, yAxes: ['p95(payload_size)']},
121+
]);
122+
});
123+
124+
it('preserves the current aggregate visualization when Seer omits one', () => {
125+
const {query} = getLogsSeerLocationQuery({
126+
currentLocationQuery: {},
127+
currentAggregateFields: [
128+
{groupBy: 'severity'},
129+
new VisualizeFunction('count(message)', {chartType: ChartType.LINE}),
130+
],
131+
pageDatetime,
132+
result: seerResult({
133+
mode: 'aggregates',
134+
groupBys: ['service.name'],
135+
sort: '-count(message)',
136+
}),
137+
});
138+
139+
expect(aggregateFields(query)).toEqual([
140+
{groupBy: 'service.name'},
141+
{chartType: ChartType.LINE, yAxes: ['count(message)']},
142+
]);
143+
});
144+
});

0 commit comments

Comments
 (0)