diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
index dfe3549eeee..dac15642d7b 100644
--- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
+++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
@@ -28,6 +28,9 @@ import { Annotation } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
jest.mock('./api/ruler');
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
const prometheusModuleSettings = { alerting: true, module: 'core:plugin/prometheus' };
diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index f58aadb9afd..3849c4708aa 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -45,6 +45,9 @@ jest.mock('@grafana/runtime', () => ({
jest.mock('./api/buildInfo');
jest.mock('./api/prometheus');
jest.mock('./api/ruler');
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
jest.spyOn(actions, 'rulesInSameGroupHaveInvalidFor').mockReturnValue([]);
jest.spyOn(apiRuler, 'rulerUrlBuilder');
diff --git a/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx
new file mode 100644
index 00000000000..d3aa6db4ec0
--- /dev/null
+++ b/public/app/features/alerting/unified/components/assistant/AnalizeRuleButton.tsx
@@ -0,0 +1,139 @@
+import { useMemo } from 'react';
+
+import { OpenAssistantProps, createAssistantContextItem, useAssistant } from '@grafana/assistant';
+import { t } from '@grafana/i18n';
+import { reportInteraction } from '@grafana/runtime';
+import { Menu } from '@grafana/ui';
+import { GrafanaAlertingRule, GrafanaRecordingRule, GrafanaRule } from 'app/types/unified-alerting';
+
+import { prometheusRuleType } from '../../utils/rules';
+
+interface AnalyzeRuleButtonProps {
+ /** Alert rule to analyze */
+ rule: GrafanaRule;
+}
+
+/**
+ * A menu item component that analyze an alert rule.
+ * Automatically creates context from alert data and opens the assistant in assistant mode.
+ */
+export function AnalyzeRuleButton(props: AnalyzeRuleButtonProps) {
+ const { isAvailable, openAssistant } = useAssistant();
+
+ if (!isAvailable || !openAssistant) {
+ return null;
+ }
+
+ return ;
+}
+
+function AnalyzeRuleButtonView({
+ rule,
+ openAssistant,
+}: AnalyzeRuleButtonProps & {
+ openAssistant: (props: OpenAssistantProps) => void;
+}) {
+ // Create alert rule context from alert rule data
+ const alertContext = useMemo(() => {
+ return createAssistantContextItem('structured', {
+ title: `Alert: ${rule.name}`,
+ data: {
+ rule: {
+ name: rule.name,
+ uid: rule.uid,
+ labels: rule.labels,
+ query: rule.query,
+ },
+ },
+ });
+ }, [rule]);
+
+ // Generate default prompt
+ const analyzeRulePrompt = useMemo(() => buildAnalyzeRulePrompt(rule), [rule]);
+
+ const handleClick = () => {
+ reportInteraction('grafana_assistant_app_analyze_rule_button_clicked', {
+ origin: 'alerting',
+ alertName: rule.name,
+ alertState: prometheusRuleType.grafana.alertingRule(rule) ? rule.state : undefined,
+ });
+
+ openAssistant({
+ origin: 'alerting',
+ mode: 'assistant',
+ prompt: analyzeRulePrompt,
+ context: [alertContext],
+ autoSend: true,
+ });
+ };
+
+ return (
+
+ );
+}
+
+/**
+ * Builds a prompt for analyzing a rule (alerting or recording).
+ * Automatically detects the rule type and uses the appropriate prompt builder.
+ */
+function buildAnalyzeRulePrompt(rule: GrafanaRule): string {
+ if (prometheusRuleType.grafana.alertingRule(rule)) {
+ return buildAnalyzeAlertingRulePrompt(rule);
+ } else if (prometheusRuleType.grafana.recordingRule(rule)) {
+ return buildAnalyzeRecordingRulePrompt(rule);
+ }
+ // Fallback (should not happen for GrafanaRule, but TypeScript requires it)
+ return `Analyze the rule "${rule.name}".`;
+}
+
+/**
+ * Builds a prompt for analyzing an alerting rule.
+ * Includes state, activeAt timestamp, annotations, and labels.
+ */
+function buildAnalyzeAlertingRulePrompt(rule: GrafanaAlertingRule): string {
+ const state = rule.state || 'firing';
+ const timeInfo = rule.activeAt ? ` starting at ${new Date(rule.activeAt).toISOString()}` : '';
+
+ let prompt = `Analyze the ${state} alert "${rule.name}"${timeInfo}.`;
+
+ const description = rule.annotations?.description || rule.annotations?.summary || '';
+ if (description) {
+ prompt += ` ${description}`;
+ }
+
+ const labelsStr = rule.labels
+ ? Object.entries(rule.labels)
+ .map(([k, v]) => `${k}="${v}"`)
+ .join(', ')
+ : '';
+ if (labelsStr) {
+ prompt += ` Labels: ${labelsStr}.`;
+ }
+
+ return prompt;
+}
+
+/**
+ * Builds a prompt for analyzing a recording rule.
+ * Includes name, query, and labels (no state or activeAt).
+ */
+function buildAnalyzeRecordingRulePrompt(rule: GrafanaRecordingRule): string {
+ const labelsStr = rule.labels
+ ? Object.entries(rule.labels)
+ .map(([k, v]) => `${k}="${v}"`)
+ .join(', ')
+ : '';
+
+ let prompt = `Analyze the recording rule "${rule.name}".`;
+
+ if (labelsStr) {
+ prompt += ` Labels: ${labelsStr}.`;
+ }
+
+ return prompt;
+}
diff --git a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
index 8834fd72580..a473eae60a1 100644
--- a/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
+++ b/public/app/features/alerting/unified/components/rule-viewer/AlertRuleMenu.tsx
@@ -1,5 +1,6 @@
import { PropsOf } from '@emotion/react';
+import { useAssistant } from '@grafana/assistant';
import { AppEvents } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
@@ -29,6 +30,7 @@ import {
rulerRuleType,
} from '../../utils/rules';
import { createRelativeUrl } from '../../utils/url';
+import { AnalyzeRuleButton } from '../assistant/AnalizeRuleButton';
import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
interface Props {
@@ -126,6 +128,9 @@ const AlertRuleMenu = ({
prometheusRuleType.alertingRule(promRule) &&
promRule.state === PromAlertingRuleState.Firing;
+ const { isAvailable: isAssistantAvailable } = useAssistant();
+ const shouldShowAnalyzeRuleButton = isAssistantAvailable && prometheusRuleType.grafana.rule(promRule);
+
const shareUrl = createShareLink(identifier);
const showDivider =
@@ -172,6 +177,7 @@ const AlertRuleMenu = ({
)}
{/* TODO Migrate Declare Incident to plugin links extensions */}
{shouldShowDeclareIncidentButton && }
+ {shouldShowAnalyzeRuleButton && }
{canDuplicate && (
({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
// metadata and interactive elements
const ELEMENTS = {
loading: byText(/Loading rule/i),
diff --git a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx
index 5de673ccb14..a88d1bef83f 100644
--- a/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleActionsButtons.test.tsx
@@ -21,6 +21,10 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { setupDataSources } from '../../testSetup/datasources';
import { fromCombinedRule, stringifyIdentifier } from '../../utils/rule-id';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
setupMswServer();
jest.mock('app/core/services/context_srv');
const mockContextSrv = jest.mocked(contextSrv);
diff --git a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
index 19100a72600..42a7594027b 100644
--- a/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleListGroupView.test.tsx
@@ -15,6 +15,10 @@ import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { RuleListGroupView } from './RuleListGroupView';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
jest.spyOn(analytics, 'logInfo');
const ui = {
diff --git a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx
index 260c7436a18..51739c6ae4c 100644
--- a/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleListStateView.test.tsx
@@ -10,6 +10,10 @@ import {
} from 'app/features/alerting/unified/mocks';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
setPluginLinksHook(() => ({
links: [],
isLoading: false,
diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
index 61c143ab843..a263b4711ca 100644
--- a/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
+++ b/public/app/features/alerting/unified/components/rules/RulesTable.test.tsx
@@ -18,6 +18,10 @@ import { mimirDataSource } from '../../mocks/server/configure';
import { RulesTable } from './RulesTable';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
jest.mock('../../hooks/useAbilities');
const mocks = {
diff --git a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx
index 90485290325..4e1bb3056fb 100644
--- a/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx
+++ b/public/app/features/alerting/unified/group-details/GroupDetailsPage.test.tsx
@@ -23,6 +23,10 @@ import { alertingFactory } from '../mocks/server/db';
import GroupDetailsPage from './GroupDetailsPage';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
jest.mock('react-virtualized-auto-sizer', () => {
return ({ children }: Props) =>
children({
diff --git a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx
index 53b80f1ebaf..a057b5030a3 100644
--- a/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx
+++ b/public/app/features/alerting/unified/rule-list/DataSourceGroupLoader.test.tsx
@@ -17,6 +17,10 @@ import { fromRulerRuleAndGroupIdentifierV2 } from '../utils/rule-id';
import { DataSourceGroupLoader } from './DataSourceGroupLoader';
import { createViewLinkFromIdentifier } from './DataSourceRuleListItem';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx
index 3a7348048da..5c8768f58ec 100644
--- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx
+++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx
@@ -12,6 +12,10 @@ import { RulesFilter } from '../search/rulesSearchParser';
import { FilterView } from './FilterView';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
diff --git a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx
index 3105c3efd6b..7385215637a 100644
--- a/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx
+++ b/public/app/features/alerting/unified/rule-list/GrafanaGroupLoader.test.tsx
@@ -1,6 +1,7 @@
import { render } from 'test/test-utils';
import { byLabelText, byRole } from 'testing-library-selector';
+import { useAssistant } from '@grafana/assistant';
import { setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
import { AccessControlAction } from 'app/types/accessControl';
import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting';
@@ -22,6 +23,12 @@ import { intervalToSeconds } from '../utils/time';
import { GrafanaGroupLoader } from './GrafanaGroupLoader';
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: jest.fn(),
+ createAssistantContextItem: jest.fn((type, data) => ({ type, ...data })),
+}));
+const mockUseAssistant = jest.mocked(useAssistant);
+
setPluginLinksHook(() => ({ links: [], isLoading: false }));
setPluginComponentsHook(() => ({ components: [], isLoading: false }));
@@ -41,11 +48,18 @@ const ui = {
export: () => byRole('menuitem', { name: /export/i }),
delete: () => byRole('menuitem', { name: /delete/i }),
pause: () => byRole('menuitem', { name: /pause/i }),
+ analyzeRule: () => byRole('menuitem', { name: /analyze rule/i }),
},
};
describe('GrafanaGroupLoader', () => {
beforeEach(() => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
grantUserPermissions([
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleDelete,
@@ -213,6 +227,68 @@ describe('GrafanaGroupLoader', () => {
const menuItems = byRole('menuitem').getAll();
expect(menuItems.length).toBe(6);
});
+
+ it('should render Analyze rule menu item when assistant is available', async () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
+
+ const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
+
+ const { user } = render(
+
+ );
+
+ const [rule1] = grafanaRulerGroup.rules;
+ const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
+
+ // Click the More button to open the menu
+ const moreButton = ui.moreButton().get(ruleListItem);
+ await user.click(moreButton);
+
+ // Check that Analyze rule menu item is present
+ expect(ui.menuItems.analyzeRule().get()).toBeInTheDocument();
+
+ // With assistant enabled, there should be 7 menu items (6 + Analyze rule)
+ const menuItems = byRole('menuitem').getAll();
+ expect(menuItems.length).toBe(7);
+ });
+
+ it('should not render Analyze rule menu item when assistant is not available', async () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ setGrafanaPromRules([rulerGroupToPromGroup(grafanaRulerGroup)]);
+
+ const groupIdentifier = getGroupIdentifier(grafanaRulerGroup);
+
+ const { user } = render(
+
+ );
+
+ const [rule1] = grafanaRulerGroup.rules;
+ const ruleListItem = await ui.ruleItem(rule1.grafana_alert.title).find();
+
+ // Click the More button to open the menu
+ const moreButton = ui.moreButton().get(ruleListItem);
+ await user.click(moreButton);
+
+ // Check that Analyze rule menu item is NOT present
+ expect(ui.menuItems.analyzeRule().query()).not.toBeInTheDocument();
+
+ // Without assistant, there should be 6 menu items
+ const menuItems = byRole('menuitem').getAll();
+ expect(menuItems.length).toBe(6);
+ });
});
function rulerGroupToPromGroup(group: RulerRuleGroupDTO): GrafanaPromRuleGroupDTO {
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx
index 15e323aa8a9..fd15ec40350 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx
@@ -40,14 +40,15 @@ import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataA
jest.mock('app/features/alerting/unified/api/prometheus');
jest.mock('app/features/alerting/unified/api/ruler');
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: () => ({ isAvailable: false, openAssistant: jest.fn() }),
+}));
+
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
jest.spyOn(ruler, 'rulerUrlBuilder');
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
-setPluginLinksHook(() => ({
- links: [],
- isLoading: false,
-}));
+setPluginLinksHook(() => ({ links: [], isLoading: false }));
const dataSources = {
prometheus: mockDataSource(
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index dad607c0de0..9f8b19218de 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -477,6 +477,7 @@
"noOptionsMessage-no-datasources-found": "No datasources found"
},
"alert-menu": {
+ "analyze-rule": "Analyze rule",
"copy-link": "Copy link",
"duplicate": "Duplicate",
"export": "Export",