From 77d762043f923a22db6d27164cf610eef838f592 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Mon, 6 Apr 2026 10:18:58 -0700 Subject: [PATCH 1/8] feat(seer): Add widget-level LLM context to dashboard widgets 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. Big number widgets also include their fetched data value. Capitalize node type headings in the backend markdown renderer for readability. Co-Authored-By: Claude Opus 4.6 --- src/sentry/seer/explorer/client_utils.py | 2 +- .../dashboards/widgetCard/index.spec.tsx | 45 ++++++++++++++++ .../app/views/dashboards/widgetCard/index.tsx | 52 ++++++++++++++++++- .../sentry/seer/explorer/test_client_utils.py | 12 ++--- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/sentry/seer/explorer/client_utils.py b/src/sentry/seer/explorer/client_utils.py index c05971dab8bc..aa8a93de729f 100644 --- a/src/sentry/seer/explorer/client_utils.py +++ b/src/sentry/seer/explorer/client_utils.py @@ -345,7 +345,7 @@ def poll_until_done( def _render_node(node: dict[str, Any], depth: int) -> str: """Recursively render an LLMContextSnapshot node and its children as markdown.""" heading = "#" * min(depth + 1, 6) - lines = [f"{heading} {node.get('nodeType', 'unknown')}"] + lines = [f"{heading} {node.get('nodeType', 'unknown').capitalize()}"] data = node.get("data") if isinstance(data, dict): diff --git a/static/app/views/dashboards/widgetCard/index.spec.tsx b/static/app/views/dashboards/widgetCard/index.spec.tsx index d421c31530a4..afba818186e5 100644 --- a/static/app/views/dashboards/widgetCard/index.spec.tsx +++ b/static/app/views/dashboards/widgetCard/index.spec.tsx @@ -1032,3 +1032,48 @@ describe('Dashboards > WidgetCard', () => { }); }); }); + +describe('getQueryHint', () => { + const {getQueryHint} = require('sentry/views/dashboards/widgetCard'); + + it.each([ + [DisplayType.LINE, 'timeseries'], + [DisplayType.AREA, 'timeseries'], + [DisplayType.BAR, 'timeseries'], + ])('returns timeseries hint for %s', (displayType, expected) => { + expect(getQueryHint(displayType)).toContain(expected); + }); + + it('returns table hint for TABLE', () => { + expect(getQueryHint(DisplayType.TABLE)).toContain('table query'); + }); + + it('returns single aggregate hint for BIG_NUMBER', () => { + expect(getQueryHint(DisplayType.BIG_NUMBER)).toContain('single aggregate'); + expect(getQueryHint(DisplayType.BIG_NUMBER)).toContain('value is included below'); + }); + + it('returns table hint as default for unknown types', () => { + expect(getQueryHint(DisplayType.WHEEL)).toContain('table query'); + }); +}); + +describe('getWidgetData', () => { + const {getWidgetData} = require('sentry/views/dashboards/widgetCard'); + + it('returns table data for BIG_NUMBER when data is available', () => { + const tableData = [{count: 42}]; + const data = {tableResults: [{title: 'test', data: tableData}]}; + expect(getWidgetData(DisplayType.BIG_NUMBER, data)).toEqual({data: tableData}); + }); + + it('returns empty object for BIG_NUMBER when no data', () => { + expect(getWidgetData(DisplayType.BIG_NUMBER, undefined)).toEqual({}); + }); + + it('returns empty object for non-BIG_NUMBER types even with data', () => { + const data = {tableResults: [{title: 'test', data: [{count: 42}]}]}; + expect(getWidgetData(DisplayType.LINE, data)).toEqual({}); + expect(getWidgetData(DisplayType.TABLE, data)).toEqual({}); + }); +}); diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index f2186e76a10c..0a7198824363 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -52,6 +52,8 @@ import {WidgetCardChartContainer} from 'sentry/views/dashboards/widgetCard/widge import type {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState'; import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types'; import {Widget} from 'sentry/views/dashboards/widgets/widget/widget'; +import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext'; +import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext'; import {useDashboardsMEPContext} from './dashboardsMEPContext'; import {VisualizationWidget} from './visualizationWidget'; @@ -140,6 +142,35 @@ type Data = { totalIssuesCount?: string; }; +/** + * Returns a hint for the Seer Explorer agent describing how to re-query this + * widget's data using a tool call, if the user wants to dig deeper. + */ +export function getQueryHint(displayType: DisplayType): string { + switch (displayType) { + case DisplayType.LINE: + case DisplayType.AREA: + case DisplayType.BAR: + return 'To dig deeper into this widget, run a timeseries query using y_axes (aggregates) + group_by (columns) + query (conditions)'; + case DisplayType.TABLE: + return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions) + sort (orderby)'; + case DisplayType.BIG_NUMBER: + return 'To dig deeper into this widget, run a single aggregate query using fields (aggregates) + query (conditions); current value is included below'; + default: + return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions)'; + } +} + +export function getWidgetData( + displayType: DisplayType, + data: Data | undefined +): Record { + if (displayType === DisplayType.BIG_NUMBER && data?.tableResults?.[0]) { + return {data: data.tableResults[0].data}; + } + return {}; +} + function WidgetCard(props: Props) { const [data, setData] = useState(); const [isLoadingTextVisible, setIsLoadingTextVisible] = useState(false); @@ -147,6 +178,22 @@ function WidgetCard(props: Props) { const {dashboardId: currentDashboardId} = useParams<{dashboardId: string}>(); const timeoutRef = useRef(null); + // Push widget metadata into the LLM context tree for Seer Explorer. + useLLMContext({ + title: props.widget.title, + displayType: props.widget.displayType, + widgetType: props.widget.widgetType, + queryHint: getQueryHint(props.widget.displayType), + queries: props.widget.queries.map(q => ({ + name: q.name, + conditions: q.conditions, + aggregates: q.aggregates, + columns: q.columns, + orderby: q.orderby, + })), + ...getWidgetData(props.widget.displayType, data), + }); + const onDataFetched = (newData: Data) => { if (props.onDataFetched) { props.onDataFetched({ @@ -435,7 +482,10 @@ function WidgetCard(props: Props) { ); } -export default withApi(withOrganization(withPageFilters(withSentryRouter(WidgetCard)))); +export default registerLLMContext( + 'widget', + withApi(withOrganization(withPageFilters(withSentryRouter(WidgetCard)))) +); function useOnDemandWarning(props: {widget: TWidget}): string | null { const organization = useOrganization(); diff --git a/tests/sentry/seer/explorer/test_client_utils.py b/tests/sentry/seer/explorer/test_client_utils.py index 2bb873038e36..83d107f92da4 100644 --- a/tests/sentry/seer/explorer/test_client_utils.py +++ b/tests/sentry/seer/explorer/test_client_utils.py @@ -200,7 +200,7 @@ def test_single_node(self) -> None: ], } result = snapshot_to_markdown(snapshot) - assert "# dashboard" in result + assert "# Dashboard" in result assert '- **title**: "Backend Health"' in result assert "- **widgetCount**: 3" in result @@ -228,9 +228,9 @@ def test_nested_nodes(self) -> None: ], } result = snapshot_to_markdown(snapshot) - assert "# dashboard" in result - assert "## widget" in result - assert "### chart" in result + assert "# Dashboard" in result + assert "## Widget" in result + assert "### Chart" in result assert '- **query**: "count()"' in result def test_empty_nodes(self) -> None: @@ -242,7 +242,7 @@ def test_node_with_no_data(self) -> None: "nodes": [{"nodeType": "dashboard", "data": None, "children": []}], } result = snapshot_to_markdown(snapshot) - assert "# dashboard" in result + assert "# Dashboard" in result assert "not an exact screenshot" in result def test_node_with_non_dict_data(self) -> None: @@ -251,5 +251,5 @@ def test_node_with_non_dict_data(self) -> None: "nodes": [{"nodeType": "widget", "data": "some string", "children": []}], } result = snapshot_to_markdown(snapshot) - assert "# widget" in result + assert "# Widget" in result assert '- "some string"' in result From cbdab9249b98b100cc72b229492d8c4fedb121a5 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Mon, 6 Apr 2026 10:24:00 -0700 Subject: [PATCH 2/8] feat(seer): Add page filters and edit mode to dashboard LLM context Include date range, environments, projects, and edit mode in the dashboard context node so the Seer Explorer agent knows the scope and timeframe the user is viewing. Co-Authored-By: Claude Opus 4.6 --- static/app/views/dashboards/dashboard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 4ece16a86e9c..729504389780 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -124,13 +124,18 @@ function DashboardInner({ const organization = useOrganization(); const api = useApi(); + const {selection} = usePageFilters(); + // Push dashboard metadata into the LLM context tree for Seer Explorer. useLLMContext({ title: dashboard.title, widgetCount: dashboard.widgets.length, filters: dashboard.filters, + isEditingDashboard, + dateRange: selection.datetime, + environments: selection.environments, + projects: selection.projects, }); - const {selection} = usePageFilters(); const {queue} = useWidgetQueryQueue(); const layouts = useMemo(() => { const desktopLayout = getDashboardLayout(dashboard.widgets); From 1bbe4ed6b88cde47062d24a189fcb4a7ee4aa827 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Mon, 6 Apr 2026 10:24:57 -0700 Subject: [PATCH 3/8] feat(seer): Add context hint to dashboard LLM context node Tell the Seer agent that dateRange/environments/projects are global page filters scoping every widget, and that child widget nodes have query config usable with telemetry_live_search. Co-Authored-By: Claude Opus 4.6 --- static/app/views/dashboards/dashboard.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 729504389780..b4500bba4baf 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -128,6 +128,8 @@ function DashboardInner({ // Push dashboard metadata into the LLM context tree for Seer Explorer. useLLMContext({ + contextHint: + '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.', title: dashboard.title, widgetCount: dashboard.widgets.length, filters: dashboard.filters, From c680da971a8ecfad10f40c7b8f4819406d0ce9a9 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Mon, 6 Apr 2026 10:35:28 -0700 Subject: [PATCH 4/8] fix(seer): Resolve TOP_N display type before LLM context capture TOP_N widgets are mutated to AREA later in the render body, but useLLMContext read the pre-mutation value on first render, producing a misleading table query hint instead of the correct timeseries hint. Co-Authored-By: Claude Opus 4.6 --- static/app/views/dashboards/widgetCard/index.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 0a7198824363..00a9ac30eff1 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -178,12 +178,19 @@ function WidgetCard(props: Props) { const {dashboardId: currentDashboardId} = useParams<{dashboardId: string}>(); const timeoutRef = useRef(null); + // Resolve TOP_N → AREA before capturing context (the render body mutates + // this later, but useLLMContext needs the resolved value on first render). + const resolvedDisplayType = + props.widget.displayType === DisplayType.TOP_N + ? DisplayType.AREA + : props.widget.displayType; + // Push widget metadata into the LLM context tree for Seer Explorer. useLLMContext({ title: props.widget.title, - displayType: props.widget.displayType, + displayType: resolvedDisplayType, widgetType: props.widget.widgetType, - queryHint: getQueryHint(props.widget.displayType), + queryHint: getQueryHint(resolvedDisplayType), queries: props.widget.queries.map(q => ({ name: q.name, conditions: q.conditions, @@ -191,7 +198,7 @@ function WidgetCard(props: Props) { columns: q.columns, orderby: q.orderby, })), - ...getWidgetData(props.widget.displayType, data), + ...getWidgetData(resolvedDisplayType, data), }); const onDataFetched = (newData: Data) => { From 50c1aa925c754a37a77e05f19302bd529bd32653 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Mon, 6 Apr 2026 10:50:22 -0700 Subject: [PATCH 5/8] fix(seer): Update endpoint test for capitalized node headings Missed this assertion when capitalizing nodeType in _render_node. Co-Authored-By: Claude Opus 4.6 --- .../seer/endpoints/test_organization_seer_explorer_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py index 0cffcbd03e37..a72dc1bfddde 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py @@ -252,7 +252,7 @@ def test_post_json_on_page_context_converted_to_markdown( assert response.status_code == 200 call_kwargs = mock_client.start_run.call_args[1] context = call_kwargs["on_page_context"] - assert "# dashboard" in context + assert "# Dashboard" in context assert '- **title**: "My Dashboard"' in context @patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient") From c2f2eecbf08df5adc77719a4254751c7ba1e8502 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Tue, 7 Apr 2026 10:52:34 -0700 Subject: [PATCH 6/8] ref(seer): Remove node heading capitalization from this PR Capitalization of nodeType headings in _render_node will be done in a separate PR. Co-Authored-By: Claude Opus 4.6 --- src/sentry/seer/explorer/client_utils.py | 2 +- .../test_organization_seer_explorer_chat.py | 2 +- tests/sentry/seer/explorer/test_client_utils.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sentry/seer/explorer/client_utils.py b/src/sentry/seer/explorer/client_utils.py index aa8a93de729f..c05971dab8bc 100644 --- a/src/sentry/seer/explorer/client_utils.py +++ b/src/sentry/seer/explorer/client_utils.py @@ -345,7 +345,7 @@ def poll_until_done( def _render_node(node: dict[str, Any], depth: int) -> str: """Recursively render an LLMContextSnapshot node and its children as markdown.""" heading = "#" * min(depth + 1, 6) - lines = [f"{heading} {node.get('nodeType', 'unknown').capitalize()}"] + lines = [f"{heading} {node.get('nodeType', 'unknown')}"] data = node.get("data") if isinstance(data, dict): diff --git a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py index a72dc1bfddde..0cffcbd03e37 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py @@ -252,7 +252,7 @@ def test_post_json_on_page_context_converted_to_markdown( assert response.status_code == 200 call_kwargs = mock_client.start_run.call_args[1] context = call_kwargs["on_page_context"] - assert "# Dashboard" in context + assert "# dashboard" in context assert '- **title**: "My Dashboard"' in context @patch("sentry.seer.endpoints.organization_seer_explorer_chat.SeerExplorerClient") diff --git a/tests/sentry/seer/explorer/test_client_utils.py b/tests/sentry/seer/explorer/test_client_utils.py index 83d107f92da4..2bb873038e36 100644 --- a/tests/sentry/seer/explorer/test_client_utils.py +++ b/tests/sentry/seer/explorer/test_client_utils.py @@ -200,7 +200,7 @@ def test_single_node(self) -> None: ], } result = snapshot_to_markdown(snapshot) - assert "# Dashboard" in result + assert "# dashboard" in result assert '- **title**: "Backend Health"' in result assert "- **widgetCount**: 3" in result @@ -228,9 +228,9 @@ def test_nested_nodes(self) -> None: ], } result = snapshot_to_markdown(snapshot) - assert "# Dashboard" in result - assert "## Widget" in result - assert "### Chart" in result + assert "# dashboard" in result + assert "## widget" in result + assert "### chart" in result assert '- **query**: "count()"' in result def test_empty_nodes(self) -> None: @@ -242,7 +242,7 @@ def test_node_with_no_data(self) -> None: "nodes": [{"nodeType": "dashboard", "data": None, "children": []}], } result = snapshot_to_markdown(snapshot) - assert "# Dashboard" in result + assert "# dashboard" in result assert "not an exact screenshot" in result def test_node_with_non_dict_data(self) -> None: @@ -251,5 +251,5 @@ def test_node_with_non_dict_data(self) -> None: "nodes": [{"nodeType": "widget", "data": "some string", "children": []}], } result = snapshot_to_markdown(snapshot) - assert "# Widget" in result + assert "# widget" in result assert '- "some string"' in result From cfa74f8313f8ceb623deb40da34437c9e3ed5661 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Tue, 7 Apr 2026 11:09:54 -0700 Subject: [PATCH 7/8] ref(seer): Move LLM helpers to own file and push BigNumber display values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move getWidgetQueryLLMHint into widgetLLMContext.tsx and remove getWidgetLLMData — visualization components now push their own parsed display values instead of extracting raw data at the WidgetCard level. Register BigNumberWidgetVisualization as a 'chart' child node under the widget and push field, value, type, unit, and thresholds into the LLM context tree. Co-Authored-By: Claude Opus 4.6 --- .../dashboards/widgetCard/index.spec.tsx | 45 ------------------- .../app/views/dashboards/widgetCard/index.tsx | 33 +------------- .../widgetCard/widgetLLMContext.spec.tsx | 28 ++++++++++++ .../widgetCard/widgetLLMContext.tsx | 20 +++++++++ .../bigNumberWidgetVisualization.tsx | 19 ++++++-- 5 files changed, 65 insertions(+), 80 deletions(-) create mode 100644 static/app/views/dashboards/widgetCard/widgetLLMContext.spec.tsx create mode 100644 static/app/views/dashboards/widgetCard/widgetLLMContext.tsx diff --git a/static/app/views/dashboards/widgetCard/index.spec.tsx b/static/app/views/dashboards/widgetCard/index.spec.tsx index afba818186e5..d421c31530a4 100644 --- a/static/app/views/dashboards/widgetCard/index.spec.tsx +++ b/static/app/views/dashboards/widgetCard/index.spec.tsx @@ -1032,48 +1032,3 @@ describe('Dashboards > WidgetCard', () => { }); }); }); - -describe('getQueryHint', () => { - const {getQueryHint} = require('sentry/views/dashboards/widgetCard'); - - it.each([ - [DisplayType.LINE, 'timeseries'], - [DisplayType.AREA, 'timeseries'], - [DisplayType.BAR, 'timeseries'], - ])('returns timeseries hint for %s', (displayType, expected) => { - expect(getQueryHint(displayType)).toContain(expected); - }); - - it('returns table hint for TABLE', () => { - expect(getQueryHint(DisplayType.TABLE)).toContain('table query'); - }); - - it('returns single aggregate hint for BIG_NUMBER', () => { - expect(getQueryHint(DisplayType.BIG_NUMBER)).toContain('single aggregate'); - expect(getQueryHint(DisplayType.BIG_NUMBER)).toContain('value is included below'); - }); - - it('returns table hint as default for unknown types', () => { - expect(getQueryHint(DisplayType.WHEEL)).toContain('table query'); - }); -}); - -describe('getWidgetData', () => { - const {getWidgetData} = require('sentry/views/dashboards/widgetCard'); - - it('returns table data for BIG_NUMBER when data is available', () => { - const tableData = [{count: 42}]; - const data = {tableResults: [{title: 'test', data: tableData}]}; - expect(getWidgetData(DisplayType.BIG_NUMBER, data)).toEqual({data: tableData}); - }); - - it('returns empty object for BIG_NUMBER when no data', () => { - expect(getWidgetData(DisplayType.BIG_NUMBER, undefined)).toEqual({}); - }); - - it('returns empty object for non-BIG_NUMBER types even with data', () => { - const data = {tableResults: [{title: 'test', data: [{count: 42}]}]}; - expect(getWidgetData(DisplayType.LINE, data)).toEqual({}); - expect(getWidgetData(DisplayType.TABLE, data)).toEqual({}); - }); -}); diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index 00a9ac30eff1..c5d91c5926e7 100644 --- a/static/app/views/dashboards/widgetCard/index.tsx +++ b/static/app/views/dashboards/widgetCard/index.tsx @@ -64,6 +64,7 @@ import { useTransactionsDeprecationWarning, } from './widgetCardContextMenu'; import {WidgetFrame} from './widgetFrame'; +import {getWidgetQueryLLMHint} from './widgetLLMContext'; export type OnDataFetchedParams = { tableResults?: TableDataWithTitle[]; @@ -142,35 +143,6 @@ type Data = { totalIssuesCount?: string; }; -/** - * Returns a hint for the Seer Explorer agent describing how to re-query this - * widget's data using a tool call, if the user wants to dig deeper. - */ -export function getQueryHint(displayType: DisplayType): string { - switch (displayType) { - case DisplayType.LINE: - case DisplayType.AREA: - case DisplayType.BAR: - return 'To dig deeper into this widget, run a timeseries query using y_axes (aggregates) + group_by (columns) + query (conditions)'; - case DisplayType.TABLE: - return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions) + sort (orderby)'; - case DisplayType.BIG_NUMBER: - return 'To dig deeper into this widget, run a single aggregate query using fields (aggregates) + query (conditions); current value is included below'; - default: - return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions)'; - } -} - -export function getWidgetData( - displayType: DisplayType, - data: Data | undefined -): Record { - if (displayType === DisplayType.BIG_NUMBER && data?.tableResults?.[0]) { - return {data: data.tableResults[0].data}; - } - return {}; -} - function WidgetCard(props: Props) { const [data, setData] = useState(); const [isLoadingTextVisible, setIsLoadingTextVisible] = useState(false); @@ -190,7 +162,7 @@ function WidgetCard(props: Props) { title: props.widget.title, displayType: resolvedDisplayType, widgetType: props.widget.widgetType, - queryHint: getQueryHint(resolvedDisplayType), + queryHint: getWidgetQueryLLMHint(resolvedDisplayType), queries: props.widget.queries.map(q => ({ name: q.name, conditions: q.conditions, @@ -198,7 +170,6 @@ function WidgetCard(props: Props) { columns: q.columns, orderby: q.orderby, })), - ...getWidgetData(resolvedDisplayType, data), }); const onDataFetched = (newData: Data) => { diff --git a/static/app/views/dashboards/widgetCard/widgetLLMContext.spec.tsx b/static/app/views/dashboards/widgetCard/widgetLLMContext.spec.tsx new file mode 100644 index 000000000000..ef531f3e34b1 --- /dev/null +++ b/static/app/views/dashboards/widgetCard/widgetLLMContext.spec.tsx @@ -0,0 +1,28 @@ +import {DisplayType} from 'sentry/views/dashboards/types'; + +import {getWidgetQueryLLMHint} from './widgetLLMContext'; + +describe('getWidgetQueryLLMHint', () => { + it.each([ + [DisplayType.LINE, 'timeseries'], + [DisplayType.AREA, 'timeseries'], + [DisplayType.BAR, 'timeseries'], + ])('returns timeseries hint for %s', (displayType, expected) => { + expect(getWidgetQueryLLMHint(displayType)).toContain(expected); + }); + + it('returns table hint for TABLE', () => { + expect(getWidgetQueryLLMHint(DisplayType.TABLE)).toContain('table query'); + }); + + it('returns single aggregate hint for BIG_NUMBER', () => { + expect(getWidgetQueryLLMHint(DisplayType.BIG_NUMBER)).toContain('single aggregate'); + expect(getWidgetQueryLLMHint(DisplayType.BIG_NUMBER)).toContain( + 'value is included below' + ); + }); + + it('returns table hint as default for unknown types', () => { + expect(getWidgetQueryLLMHint(DisplayType.WHEEL)).toContain('table query'); + }); +}); diff --git a/static/app/views/dashboards/widgetCard/widgetLLMContext.tsx b/static/app/views/dashboards/widgetCard/widgetLLMContext.tsx new file mode 100644 index 000000000000..c792a7ed1b34 --- /dev/null +++ b/static/app/views/dashboards/widgetCard/widgetLLMContext.tsx @@ -0,0 +1,20 @@ +import {DisplayType} from 'sentry/views/dashboards/types'; + +/** + * Returns a hint for the Seer Explorer agent describing how to re-query this + * widget's data using a tool call, if the user wants to dig deeper. + */ +export function getWidgetQueryLLMHint(displayType: DisplayType): string { + switch (displayType) { + case DisplayType.LINE: + case DisplayType.AREA: + case DisplayType.BAR: + return 'To dig deeper into this widget, run a timeseries query using y_axes (aggregates) + group_by (columns) + query (conditions)'; + case DisplayType.TABLE: + return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions) + sort (orderby)'; + case DisplayType.BIG_NUMBER: + return 'To dig deeper into this widget, run a single aggregate query using fields (aggregates) + query (conditions); current value is included below'; + default: + return 'To dig deeper into this widget, run a table query using fields (aggregates + columns) + query (conditions)'; + } +} diff --git a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx index b3062ebd0670..4322b08cde03 100644 --- a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx @@ -18,6 +18,8 @@ import type { TabularValueUnit, Thresholds, } from 'sentry/views/dashboards/widgets/common/types'; +import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext'; +import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext'; import {DEEMPHASIS_VARIANT, LOADING_PLACEHOLDER} from './settings'; import {ThresholdsIndicator} from './thresholdsIndicator'; @@ -33,7 +35,7 @@ interface BigNumberWidgetVisualizationProps { unit?: TabularValueUnit; } -export function BigNumberWidgetVisualization(props: BigNumberWidgetVisualizationProps) { +function BigNumberWidgetVisualizationInner(props: BigNumberWidgetVisualizationProps) { const { field, value, @@ -44,6 +46,10 @@ export function BigNumberWidgetVisualization(props: BigNumberWidgetVisualization unit, } = props; + // Push parsed display values into the LLM context tree for Seer Explorer. + // These are already computed by the parent — no raw data involved. + useLLMContext({field, value, type, unit, thresholds: props.thresholds}); + const theme = useTheme(); if ((typeof value === 'number' && !Number.isFinite(value)) || Number.isNaN(value)) { @@ -206,6 +212,11 @@ const LoadingPlaceholder = styled('span')` font-size: ${p => p.theme.font.size.lg}; `; -BigNumberWidgetVisualization.LoadingPlaceholder = function () { - return {LOADING_PLACEHOLDER}; -}; +export const BigNumberWidgetVisualization = Object.assign( + registerLLMContext('chart', BigNumberWidgetVisualizationInner), + { + LoadingPlaceholder() { + return {LOADING_PLACEHOLDER}; + }, + } +); From dad0a6b2f02f4bfb892543a88adf5456fe4e0176 Mon Sep 17 00:00:00 2001 From: Mihir-Mavalankar Date: Tue, 7 Apr 2026 11:14:48 -0700 Subject: [PATCH 8/8] fix(seer): Fix TypeScript error in BigNumber LLM context registration Change BigNumberWidgetVisualizationProps from interface to type so it satisfies the Record constraint in registerLLMContext. Structurally identical, no behavior change. Co-Authored-By: Claude Opus 4.6 --- .../widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx index 4322b08cde03..86bfe284a386 100644 --- a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx @@ -24,7 +24,7 @@ import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLM import {DEEMPHASIS_VARIANT, LOADING_PLACEHOLDER} from './settings'; import {ThresholdsIndicator} from './thresholdsIndicator'; -interface BigNumberWidgetVisualizationProps { +type BigNumberWidgetVisualizationProps = { field: string; value: number | string; maximumValue?: number; @@ -33,7 +33,7 @@ interface BigNumberWidgetVisualizationProps { thresholds?: Thresholds; type?: TabularValueType; unit?: TabularValueUnit; -} +}; function BigNumberWidgetVisualizationInner(props: BigNumberWidgetVisualizationProps) { const {