Skip to content

Commit 0514751

Browse files
Mihir-MavalankarClaude Opus 4.6
andauthored
feat(seer): Add widget-level LLM context to dashboard widgets (#112267)
+ Register each dashboard widget as a child node in the LLM context tree so the Seer Explorer agent gets per-widget metadata: title, display type, widget type, query config, and tool-use hints that map directly to Seer's telemetry_live_search parameters. + Add page-level filters (date range, environments, projects) and edit mode to the dashboard context node so the agent knows the scope and timeframe the user is viewing. + For BigNumber widgets, the visualization component registers as a chart child node and pushes parsed display values (field, value, type, unit, thresholds) — no raw data, just what's rendered on screen. --------- Co-authored-by: Claude Opus 4.6 <noreply@example.com>
1 parent 1de477a commit 0514751

File tree

5 files changed

+102
-8
lines changed

5 files changed

+102
-8
lines changed

static/app/views/dashboards/dashboard.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,20 @@ function DashboardInner({
124124
const organization = useOrganization();
125125
const api = useApi();
126126

127+
const {selection} = usePageFilters();
128+
127129
// Push dashboard metadata into the LLM context tree for Seer Explorer.
128130
useLLMContext({
131+
contextHint:
132+
'This is a Sentry dashboard. The dateRange, environments, and projects below are global page filters that scope every widget query. Each child widget node contains its own query config that can be used with the telemetry_live_search tool to fetch or drill into its data.',
129133
title: dashboard.title,
130134
widgetCount: dashboard.widgets.length,
131135
filters: dashboard.filters,
136+
isEditingDashboard,
137+
dateRange: selection.datetime,
138+
environments: selection.environments,
139+
projects: selection.projects,
132140
});
133-
const {selection} = usePageFilters();
134141
const {queue} = useWidgetQueryQueue();
135142
const layouts = useMemo<LayoutState>(() => {
136143
const desktopLayout = getDashboardLayout(dashboard.widgets);

static/app/views/dashboards/widgetCard/index.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widge
5252
import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState';
5353
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
5454
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
55+
import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext';
56+
import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext';
5557

5658
import {useDashboardsMEPContext} from './dashboardsMEPContext';
5759
import {VisualizationWidget} from './visualizationWidget';
@@ -62,6 +64,7 @@ import {
6264
useTransactionsDeprecationWarning,
6365
} from './widgetCardContextMenu';
6466
import {WidgetFrame} from './widgetFrame';
67+
import {getWidgetQueryLLMHint} from './widgetLLMContext';
6568

6669
export type OnDataFetchedParams = {
6770
tableResults?: TableDataWithTitle[];
@@ -147,6 +150,28 @@ function WidgetCard(props: Props) {
147150
const {dashboardId: currentDashboardId} = useParams<{dashboardId: string}>();
148151
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
149152

153+
// Resolve TOP_N → AREA before capturing context (the render body mutates
154+
// this later, but useLLMContext needs the resolved value on first render).
155+
const resolvedDisplayType =
156+
props.widget.displayType === DisplayType.TOP_N
157+
? DisplayType.AREA
158+
: props.widget.displayType;
159+
160+
// Push widget metadata into the LLM context tree for Seer Explorer.
161+
useLLMContext({
162+
title: props.widget.title,
163+
displayType: resolvedDisplayType,
164+
widgetType: props.widget.widgetType,
165+
queryHint: getWidgetQueryLLMHint(resolvedDisplayType),
166+
queries: props.widget.queries.map(q => ({
167+
name: q.name,
168+
conditions: q.conditions,
169+
aggregates: q.aggregates,
170+
columns: q.columns,
171+
orderby: q.orderby,
172+
})),
173+
});
174+
150175
const onDataFetched = (newData: Data) => {
151176
if (props.onDataFetched) {
152177
props.onDataFetched({
@@ -435,7 +460,10 @@ function WidgetCard(props: Props) {
435460
);
436461
}
437462

438-
export default withApi(withOrganization(withPageFilters(withSentryRouter(WidgetCard))));
463+
export default registerLLMContext(
464+
'widget',
465+
withApi(withOrganization(withPageFilters(withSentryRouter(WidgetCard))))
466+
);
439467

440468
function useOnDemandWarning(props: {widget: TWidget}): string | null {
441469
const organization = useOrganization();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {DisplayType} from 'sentry/views/dashboards/types';
2+
3+
import {getWidgetQueryLLMHint} from './widgetLLMContext';
4+
5+
describe('getWidgetQueryLLMHint', () => {
6+
it.each([
7+
[DisplayType.LINE, 'timeseries'],
8+
[DisplayType.AREA, 'timeseries'],
9+
[DisplayType.BAR, 'timeseries'],
10+
])('returns timeseries hint for %s', (displayType, expected) => {
11+
expect(getWidgetQueryLLMHint(displayType)).toContain(expected);
12+
});
13+
14+
it('returns table hint for TABLE', () => {
15+
expect(getWidgetQueryLLMHint(DisplayType.TABLE)).toContain('table query');
16+
});
17+
18+
it('returns single aggregate hint for BIG_NUMBER', () => {
19+
expect(getWidgetQueryLLMHint(DisplayType.BIG_NUMBER)).toContain('single aggregate');
20+
expect(getWidgetQueryLLMHint(DisplayType.BIG_NUMBER)).toContain(
21+
'value is included below'
22+
);
23+
});
24+
25+
it('returns table hint as default for unknown types', () => {
26+
expect(getWidgetQueryLLMHint(DisplayType.WHEEL)).toContain('table query');
27+
});
28+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {DisplayType} from 'sentry/views/dashboards/types';
2+
3+
/**
4+
* Returns a hint for the Seer Explorer agent describing how to re-query this
5+
* widget's data using a tool call, if the user wants to dig deeper.
6+
*/
7+
export function getWidgetQueryLLMHint(displayType: DisplayType): string {
8+
switch (displayType) {
9+
case DisplayType.LINE:
10+
case DisplayType.AREA:
11+
case DisplayType.BAR:
12+
return 'To dig deeper into this widget, run a timeseries query using y_axes (aggregates) + group_by (columns) + query (conditions)';
13+
case DisplayType.TABLE:
14+
return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions) + sort (orderby)';
15+
case DisplayType.BIG_NUMBER:
16+
return 'To dig deeper into this widget, run a single aggregate query using fields (aggregates) + query (conditions); current value is included below';
17+
default:
18+
return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions)';
19+
}
20+
}

static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import type {
1818
TabularValueUnit,
1919
Thresholds,
2020
} from 'sentry/views/dashboards/widgets/common/types';
21+
import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext';
22+
import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext';
2123

2224
import {DEEMPHASIS_VARIANT, LOADING_PLACEHOLDER} from './settings';
2325
import {ThresholdsIndicator} from './thresholdsIndicator';
2426

25-
interface BigNumberWidgetVisualizationProps {
27+
type BigNumberWidgetVisualizationProps = {
2628
field: string;
2729
value: number | string;
2830
maximumValue?: number;
@@ -31,9 +33,9 @@ interface BigNumberWidgetVisualizationProps {
3133
thresholds?: Thresholds;
3234
type?: TabularValueType;
3335
unit?: TabularValueUnit;
34-
}
36+
};
3537

36-
export function BigNumberWidgetVisualization(props: BigNumberWidgetVisualizationProps) {
38+
function BigNumberWidgetVisualizationInner(props: BigNumberWidgetVisualizationProps) {
3739
const {
3840
field,
3941
value,
@@ -44,6 +46,10 @@ export function BigNumberWidgetVisualization(props: BigNumberWidgetVisualization
4446
unit,
4547
} = props;
4648

49+
// Push parsed display values into the LLM context tree for Seer Explorer.
50+
// These are already computed by the parent — no raw data involved.
51+
useLLMContext({field, value, type, unit, thresholds: props.thresholds});
52+
4753
const theme = useTheme();
4854

4955
if ((typeof value === 'number' && !Number.isFinite(value)) || Number.isNaN(value)) {
@@ -206,6 +212,11 @@ const LoadingPlaceholder = styled('span')`
206212
font-size: ${p => p.theme.font.size.lg};
207213
`;
208214

209-
BigNumberWidgetVisualization.LoadingPlaceholder = function () {
210-
return <LoadingPlaceholder>{LOADING_PLACEHOLDER}</LoadingPlaceholder>;
211-
};
215+
export const BigNumberWidgetVisualization = Object.assign(
216+
registerLLMContext('chart', BigNumberWidgetVisualizationInner),
217+
{
218+
LoadingPlaceholder() {
219+
return <LoadingPlaceholder>{LOADING_PLACEHOLDER}</LoadingPlaceholder>;
220+
},
221+
}
222+
);

0 commit comments

Comments
 (0)