diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index 4ece16a86e9c..b4500bba4baf 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/static/app/views/dashboards/dashboard.tsx @@ -124,13 +124,20 @@ function DashboardInner({ const organization = useOrganization(); const api = useApi(); + const {selection} = usePageFilters(); + // 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, + isEditingDashboard, + dateRange: selection.datetime, + environments: selection.environments, + projects: selection.projects, }); - const {selection} = usePageFilters(); const {queue} = useWidgetQueryQueue(); const layouts = useMemo(() => { const desktopLayout = getDashboardLayout(dashboard.widgets); diff --git a/static/app/views/dashboards/widgetCard/index.tsx b/static/app/views/dashboards/widgetCard/index.tsx index f2186e76a10c..c5d91c5926e7 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'; @@ -62,6 +64,7 @@ import { useTransactionsDeprecationWarning, } from './widgetCardContextMenu'; import {WidgetFrame} from './widgetFrame'; +import {getWidgetQueryLLMHint} from './widgetLLMContext'; export type OnDataFetchedParams = { tableResults?: TableDataWithTitle[]; @@ -147,6 +150,28 @@ 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: resolvedDisplayType, + widgetType: props.widget.widgetType, + queryHint: getWidgetQueryLLMHint(resolvedDisplayType), + queries: props.widget.queries.map(q => ({ + name: q.name, + conditions: q.conditions, + aggregates: q.aggregates, + columns: q.columns, + orderby: q.orderby, + })), + }); + const onDataFetched = (newData: Data) => { if (props.onDataFetched) { props.onDataFetched({ @@ -435,7 +460,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/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..86bfe284a386 100644 --- a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx +++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.tsx @@ -18,11 +18,13 @@ 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'; -interface BigNumberWidgetVisualizationProps { +type BigNumberWidgetVisualizationProps = { field: string; value: number | string; maximumValue?: number; @@ -31,9 +33,9 @@ interface BigNumberWidgetVisualizationProps { thresholds?: Thresholds; type?: TabularValueType; 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}; + }, + } +);