ruleForm.spec.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. import {EventsStatsFixture} from 'sentry-fixture/events';
  2. import {IncidentTriggerFixture} from 'sentry-fixture/incidentTrigger';
  3. import {MetricRuleFixture} from 'sentry-fixture/metricRule';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import selectEvent from 'sentry-test/selectEvent';
  7. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  8. import type FormModel from 'sentry/components/forms/model';
  9. import ProjectsStore from 'sentry/stores/projectsStore';
  10. import {ActivationConditionType, MonitorType} from 'sentry/types/alerts';
  11. import {metric} from 'sentry/utils/analytics';
  12. import RuleFormContainer from 'sentry/views/alerts/rules/metric/ruleForm';
  13. import {
  14. AlertRuleComparisonType,
  15. AlertRuleSeasonality,
  16. AlertRuleSensitivity,
  17. Dataset,
  18. } from 'sentry/views/alerts/rules/metric/types';
  19. import {permissionAlertText} from 'sentry/views/settings/project/permissionAlert';
  20. jest.mock('sentry/actionCreators/indicator');
  21. jest.mock('sentry/utils/analytics', () => ({
  22. metric: {
  23. startSpan: jest.fn(() => ({
  24. setTag: jest.fn(),
  25. setData: jest.fn(),
  26. })),
  27. endSpan: jest.fn(),
  28. },
  29. trackAnalytics: jest.fn(),
  30. }));
  31. describe('Incident Rules Form', () => {
  32. let organization, project, router, location, anomalies;
  33. // create wrapper
  34. const createWrapper = props =>
  35. render(
  36. <RuleFormContainer
  37. params={{orgId: organization.slug, projectId: project.slug}}
  38. organization={organization}
  39. location={location}
  40. project={project}
  41. {...props}
  42. />,
  43. {router, organization}
  44. );
  45. beforeEach(() => {
  46. const initialData = initializeOrg({
  47. organization: {features: ['metric-alert-threshold-period', 'change-alerts']},
  48. });
  49. organization = initialData.organization;
  50. project = initialData.project;
  51. location = initialData.router.location;
  52. ProjectsStore.loadInitialData([project]);
  53. router = initialData.router;
  54. MockApiClient.addMockResponse({
  55. url: '/organizations/org-slug/tags/',
  56. body: [],
  57. });
  58. MockApiClient.addMockResponse({
  59. url: '/organizations/org-slug/users/',
  60. body: [],
  61. });
  62. MockApiClient.addMockResponse({
  63. url: '/projects/org-slug/project-slug/environments/',
  64. body: [],
  65. });
  66. MockApiClient.addMockResponse({
  67. url: '/organizations/org-slug/events-stats/',
  68. body: EventsStatsFixture({
  69. isMetricsData: true,
  70. }),
  71. });
  72. MockApiClient.addMockResponse({
  73. url: '/organizations/org-slug/events-meta/',
  74. body: {count: 5},
  75. });
  76. MockApiClient.addMockResponse({
  77. url: '/organizations/org-slug/alert-rules/available-actions/',
  78. body: [
  79. {
  80. allowedTargetTypes: ['user', 'team'],
  81. integrationName: null,
  82. type: 'email',
  83. integrationId: null,
  84. },
  85. ],
  86. });
  87. MockApiClient.addMockResponse({
  88. url: '/organizations/org-slug/metrics-estimation-stats/',
  89. body: EventsStatsFixture(),
  90. });
  91. MockApiClient.addMockResponse({
  92. url: '/organizations/org-slug/metrics/meta/',
  93. body: [],
  94. });
  95. MockApiClient.addMockResponse({
  96. url: '/organizations/org-slug/metrics/tags/',
  97. body: [],
  98. });
  99. MockApiClient.addMockResponse({
  100. method: 'GET',
  101. url: '/organizations/org-slug/recent-searches/',
  102. body: [],
  103. });
  104. anomalies = MockApiClient.addMockResponse({
  105. method: 'POST',
  106. url: '/organizations/org-slug/events/anomalies/',
  107. body: [],
  108. });
  109. MockApiClient.addMockResponse({
  110. url: `/organizations/${organization.slug}/spans/fields/`,
  111. method: 'GET',
  112. });
  113. });
  114. afterEach(() => {
  115. MockApiClient.clearMockResponses();
  116. jest.clearAllMocks();
  117. });
  118. describe('Viewing the rule', () => {
  119. const rule = MetricRuleFixture();
  120. it('is enabled without org-level alerts:write', async () => {
  121. organization.access = [];
  122. project.access = [];
  123. createWrapper({rule});
  124. expect(await screen.findByText(permissionAlertText)).toBeInTheDocument();
  125. expect(screen.queryByLabelText('Save Rule')).toBeDisabled();
  126. });
  127. it('is enabled with org-level alerts:write', async () => {
  128. organization.access = ['alerts:write'];
  129. project.access = [];
  130. createWrapper({rule});
  131. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  132. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  133. });
  134. it('is enabled with project-level alerts:write', async () => {
  135. organization.access = [];
  136. project.access = ['alerts:write'];
  137. createWrapper({rule});
  138. expect(await screen.findByLabelText('Save Rule')).toBeEnabled();
  139. expect(screen.queryByText(permissionAlertText)).not.toBeInTheDocument();
  140. });
  141. it('renders time window', async () => {
  142. createWrapper({rule});
  143. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  144. });
  145. it('renders time window for activated alerts', async () => {
  146. createWrapper({
  147. rule: {
  148. ...rule,
  149. monitorType: MonitorType.CONTINUOUS,
  150. },
  151. });
  152. expect(await screen.findByText('1 hour interval')).toBeInTheDocument();
  153. });
  154. });
  155. describe('Creating a new rule', () => {
  156. let createRule;
  157. beforeEach(() => {
  158. ProjectsStore.loadInitialData([
  159. project,
  160. {
  161. ...project,
  162. id: '10',
  163. slug: 'project-slug-2',
  164. },
  165. ]);
  166. createRule = MockApiClient.addMockResponse({
  167. url: '/organizations/org-slug/alert-rules/',
  168. method: 'POST',
  169. });
  170. MockApiClient.addMockResponse({
  171. url: '/projects/org-slug/project-slug-2/environments/',
  172. body: [],
  173. });
  174. });
  175. /**
  176. * Note this isn't necessarily the desired behavior, as it is just documenting the behavior
  177. */
  178. it('creates a rule', async () => {
  179. const rule = MetricRuleFixture();
  180. createWrapper({
  181. rule: {
  182. ...rule,
  183. id: undefined,
  184. eventTypes: ['default'],
  185. },
  186. });
  187. // Clear field
  188. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  189. // Enter in name so we can submit
  190. await userEvent.type(
  191. screen.getByPlaceholderText('Enter Alert Name'),
  192. 'Incident Rule'
  193. );
  194. // Set thresholdPeriod
  195. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  196. await userEvent.click(screen.getByLabelText('Save Rule'));
  197. expect(createRule).toHaveBeenCalledWith(
  198. expect.anything(),
  199. expect.objectContaining({
  200. data: expect.objectContaining({
  201. name: 'Incident Rule',
  202. projects: ['project-slug'],
  203. eventTypes: ['default'],
  204. thresholdPeriod: 10,
  205. }),
  206. })
  207. );
  208. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  209. });
  210. it('can create a rule for a different project', async () => {
  211. const rule = MetricRuleFixture();
  212. createWrapper({
  213. rule: {
  214. ...rule,
  215. id: undefined,
  216. eventTypes: ['default'],
  217. },
  218. });
  219. // Clear field
  220. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  221. // Enter in name so we can submit
  222. await userEvent.type(
  223. screen.getByPlaceholderText('Enter Alert Name'),
  224. 'Incident Rule'
  225. );
  226. // Change project
  227. await userEvent.click(screen.getByText('project-slug'));
  228. await userEvent.click(screen.getByText('project-slug-2'));
  229. await userEvent.click(screen.getByLabelText('Save Rule'));
  230. expect(createRule).toHaveBeenCalledWith(
  231. expect.anything(),
  232. expect.objectContaining({
  233. data: expect.objectContaining({
  234. name: 'Incident Rule',
  235. projects: ['project-slug-2'],
  236. }),
  237. })
  238. );
  239. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  240. });
  241. it('creates a rule with generic_metrics dataset', async () => {
  242. organization.features = [...organization.features, 'mep-rollout-flag'];
  243. const rule = MetricRuleFixture();
  244. createWrapper({
  245. rule: {
  246. ...rule,
  247. id: undefined,
  248. aggregate: 'count()',
  249. eventTypes: ['transaction'],
  250. dataset: 'transactions',
  251. },
  252. });
  253. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  254. await userEvent.click(screen.getByLabelText('Save Rule'));
  255. expect(createRule).toHaveBeenCalledWith(
  256. expect.anything(),
  257. expect.objectContaining({
  258. data: expect.objectContaining({
  259. name: 'My Incident Rule',
  260. projects: ['project-slug'],
  261. aggregate: 'count()',
  262. eventTypes: ['transaction'],
  263. dataset: 'generic_metrics',
  264. thresholdPeriod: 1,
  265. }),
  266. })
  267. );
  268. });
  269. // Activation condition
  270. it('creates a rule with an activation condition', async () => {
  271. organization.features = [
  272. ...organization.features,
  273. 'mep-rollout-flag',
  274. 'activated-alert-rules',
  275. ];
  276. const rule = MetricRuleFixture({
  277. monitorType: MonitorType.ACTIVATED,
  278. activationCondition: ActivationConditionType.RELEASE_CREATION,
  279. });
  280. createWrapper({
  281. rule: {
  282. ...rule,
  283. id: undefined,
  284. aggregate: 'count()',
  285. eventTypes: ['transaction'],
  286. dataset: 'transactions',
  287. },
  288. });
  289. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  290. await userEvent.click(screen.getByLabelText('Save Rule'));
  291. expect(createRule).toHaveBeenCalledWith(
  292. expect.anything(),
  293. expect.objectContaining({
  294. data: expect.objectContaining({
  295. name: 'My Incident Rule',
  296. projects: ['project-slug'],
  297. aggregate: 'count()',
  298. eventTypes: ['transaction'],
  299. dataset: 'generic_metrics',
  300. thresholdPeriod: 1,
  301. }),
  302. })
  303. );
  304. });
  305. it('creates a continuous rule with activated rules enabled', async () => {
  306. organization.features = [
  307. ...organization.features,
  308. 'mep-rollout-flag',
  309. 'activated-alert-rules',
  310. ];
  311. const rule = MetricRuleFixture({
  312. monitorType: MonitorType.CONTINUOUS,
  313. });
  314. createWrapper({
  315. rule: {
  316. ...rule,
  317. id: undefined,
  318. aggregate: 'count()',
  319. eventTypes: ['transaction'],
  320. dataset: 'transactions',
  321. },
  322. });
  323. expect(await screen.findByTestId('alert-total-events')).toHaveTextContent('Total5');
  324. await userEvent.click(screen.getByLabelText('Save Rule'));
  325. expect(createRule).toHaveBeenCalledWith(
  326. expect.anything(),
  327. expect.objectContaining({
  328. data: expect.objectContaining({
  329. name: 'My Incident Rule',
  330. projects: ['project-slug'],
  331. aggregate: 'count()',
  332. eventTypes: ['transaction'],
  333. dataset: 'generic_metrics',
  334. thresholdPeriod: 1,
  335. }),
  336. })
  337. );
  338. });
  339. it('creates an anomaly detection rule', async () => {
  340. organization.features = [
  341. ...organization.features,
  342. 'anomaly-detection-alerts',
  343. 'anomaly-detection-rollout',
  344. ];
  345. const rule = MetricRuleFixture({
  346. sensitivity: AlertRuleSensitivity.MEDIUM,
  347. seasonality: AlertRuleSeasonality.AUTO,
  348. });
  349. createWrapper({
  350. rule: {
  351. ...rule,
  352. id: undefined,
  353. aggregate: 'count()',
  354. eventTypes: ['error'],
  355. dataset: 'events',
  356. },
  357. });
  358. expect(
  359. await screen.findByRole('textbox', {name: 'Level of responsiveness'})
  360. ).toBeInTheDocument();
  361. expect(anomalies).toHaveBeenLastCalledWith(
  362. expect.anything(),
  363. expect.objectContaining({
  364. data: expect.objectContaining({
  365. config: {
  366. direction: 'up',
  367. sensitivity: AlertRuleSensitivity.MEDIUM,
  368. expected_seasonality: AlertRuleSeasonality.AUTO,
  369. time_period: 60,
  370. },
  371. }),
  372. })
  373. );
  374. await userEvent.click(screen.getByLabelText('Save Rule'));
  375. expect(createRule).toHaveBeenLastCalledWith(
  376. expect.anything(),
  377. expect.objectContaining({
  378. data: expect.objectContaining({
  379. aggregate: 'count()',
  380. dataset: 'events',
  381. environment: null,
  382. eventTypes: ['error'],
  383. detectionType: AlertRuleComparisonType.DYNAMIC,
  384. sensitivity: AlertRuleSensitivity.MEDIUM,
  385. seasonality: AlertRuleSeasonality.AUTO,
  386. }),
  387. })
  388. );
  389. });
  390. it('switches to custom metric and selects event.type:error', async () => {
  391. organization.features = [...organization.features, 'performance-view'];
  392. const rule = MetricRuleFixture();
  393. createWrapper({
  394. rule: {
  395. ...rule,
  396. id: undefined,
  397. eventTypes: ['default'],
  398. },
  399. });
  400. await userEvent.click(screen.getAllByText('Number of Errors').at(1)!);
  401. await userEvent.click(await screen.findByText('Custom Measurement'));
  402. await userEvent.click(screen.getAllByText('event.type:transaction').at(1)!);
  403. await userEvent.click(await screen.findByText('event.type:error'));
  404. expect(screen.getAllByText('Custom Measurement')).toHaveLength(2);
  405. await userEvent.click(screen.getByLabelText('Save Rule'));
  406. expect(createRule).toHaveBeenLastCalledWith(
  407. expect.anything(),
  408. expect.objectContaining({
  409. data: expect.objectContaining({
  410. aggregate: 'count()',
  411. alertType: 'custom_transactions',
  412. dataset: 'events',
  413. datasource: 'error',
  414. environment: null,
  415. eventTypes: ['error'],
  416. name: 'My Incident Rule',
  417. projectId: '2',
  418. projects: ['project-slug'],
  419. query: '',
  420. }),
  421. })
  422. );
  423. });
  424. it('creates an insights metric rule', async () => {
  425. const rule = MetricRuleFixture();
  426. createWrapper({
  427. rule: {
  428. ...rule,
  429. id: undefined,
  430. eventTypes: ['transaction'],
  431. aggregate: 'avg(d:spans/exclusive_time@millisecond)',
  432. dataset: Dataset.GENERIC_METRICS,
  433. },
  434. });
  435. // Clear field
  436. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  437. // Enter in name so we can submit
  438. await userEvent.type(
  439. screen.getByPlaceholderText('Enter Alert Name'),
  440. 'Insights Incident Rule'
  441. );
  442. // Set thresholdPeriod
  443. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  444. await userEvent.click(screen.getByLabelText('Save Rule'));
  445. expect(createRule).toHaveBeenCalledWith(
  446. expect.anything(),
  447. expect.objectContaining({
  448. data: expect.objectContaining({
  449. name: 'Insights Incident Rule',
  450. projects: ['project-slug'],
  451. eventTypes: ['transaction'],
  452. thresholdPeriod: 10,
  453. alertType: 'insights_metrics',
  454. dataset: 'generic_metrics',
  455. }),
  456. })
  457. );
  458. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  459. });
  460. it('creates an EAP metric rule', async () => {
  461. const rule = MetricRuleFixture();
  462. createWrapper({
  463. rule: {
  464. ...rule,
  465. id: undefined,
  466. eventTypes: [],
  467. aggregate: 'count(span.duration)',
  468. dataset: Dataset.EVENTS_ANALYTICS_PLATFORM,
  469. },
  470. });
  471. // Clear field
  472. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  473. // Enter in name so we can submit
  474. await userEvent.type(
  475. screen.getByPlaceholderText('Enter Alert Name'),
  476. 'EAP Incident Rule'
  477. );
  478. // Set thresholdPeriod
  479. await selectEvent.select(screen.getAllByText('For 1 minute')[0], 'For 10 minutes');
  480. await userEvent.click(screen.getByLabelText('Save Rule'));
  481. expect(createRule).toHaveBeenCalledWith(
  482. expect.anything(),
  483. expect.objectContaining({
  484. data: expect.objectContaining({
  485. name: 'EAP Incident Rule',
  486. projects: ['project-slug'],
  487. eventTypes: [],
  488. thresholdPeriod: 10,
  489. alertType: 'eap_metrics',
  490. dataset: 'events_analytics_platform',
  491. }),
  492. })
  493. );
  494. expect(metric.startSpan).toHaveBeenCalledWith({name: 'saveAlertRule'});
  495. });
  496. });
  497. describe('Editing a rule', () => {
  498. let editRule;
  499. let editTrigger;
  500. const rule = MetricRuleFixture();
  501. beforeEach(() => {
  502. editRule = MockApiClient.addMockResponse({
  503. url: `/organizations/org-slug/alert-rules/${rule.id}/`,
  504. method: 'PUT',
  505. body: rule,
  506. });
  507. editTrigger = MockApiClient.addMockResponse({
  508. url: `/organizations/org-slug/alert-rules/${rule.id}/triggers/1/`,
  509. method: 'PUT',
  510. body: IncidentTriggerFixture({id: '1'}),
  511. });
  512. });
  513. afterEach(() => {
  514. editRule.mockReset();
  515. editTrigger.mockReset();
  516. });
  517. it('edits metric', async () => {
  518. createWrapper({
  519. ruleId: rule.id,
  520. rule,
  521. });
  522. // Clear field
  523. await userEvent.clear(screen.getByPlaceholderText('Enter Alert Name'));
  524. await userEvent.type(screen.getByPlaceholderText('Enter Alert Name'), 'new name');
  525. await userEvent.click(screen.getByLabelText('Save Rule'));
  526. expect(editRule).toHaveBeenLastCalledWith(
  527. expect.anything(),
  528. expect.objectContaining({
  529. data: expect.objectContaining({
  530. name: 'new name',
  531. }),
  532. })
  533. );
  534. });
  535. it('edits query', async () => {
  536. createWrapper({
  537. name: 'Query Rule',
  538. projects: ['project-slug'],
  539. eventTypes: ['num_errors'],
  540. thresholdPeriod: 10,
  541. query: 'is:unresolved',
  542. rule,
  543. ruleId: rule.id,
  544. });
  545. const queryInput = await screen.findByTestId('query-builder-input');
  546. await userEvent.type(queryInput, 'has:http.url');
  547. await userEvent.type(queryInput, '{enter}');
  548. await userEvent.click(screen.getByLabelText('Save Rule'));
  549. expect(editRule).toHaveBeenLastCalledWith(
  550. expect.anything(),
  551. expect.objectContaining({
  552. data: expect.objectContaining({
  553. query: 'has:http.url',
  554. }),
  555. })
  556. );
  557. }, 10000);
  558. it('switches from percent change to count', async () => {
  559. createWrapper({
  560. ruleId: rule.id,
  561. rule: {
  562. ...rule,
  563. timeWindow: 60,
  564. comparisonDelta: 100,
  565. eventTypes: ['error'],
  566. resolution: 2,
  567. },
  568. });
  569. expect(screen.getByLabelText('Static: above or below {x}')).not.toBeChecked();
  570. await userEvent.click(screen.getByText('Static: above or below {x}'));
  571. await waitFor(() =>
  572. expect(screen.getByLabelText('Static: above or below {x}')).toBeChecked()
  573. );
  574. await userEvent.click(screen.getByLabelText('Save Rule'));
  575. expect(editRule).toHaveBeenLastCalledWith(
  576. expect.anything(),
  577. expect.objectContaining({
  578. data: expect.objectContaining({
  579. // Comparison delta is reset
  580. comparisonDelta: null,
  581. }),
  582. })
  583. );
  584. });
  585. it('switches to anomaly detection threshold', async () => {
  586. organization.features = [
  587. ...organization.features,
  588. 'anomaly-detection-alerts',
  589. 'anomaly-detection-rollout',
  590. ];
  591. createWrapper({
  592. rule: {
  593. ...rule,
  594. id: undefined,
  595. eventTypes: ['default'],
  596. },
  597. });
  598. const anomaly_option = await screen.findByText(
  599. 'Anomaly: whenever values are outside of expected bounds'
  600. );
  601. expect(anomaly_option).toBeInTheDocument();
  602. });
  603. it('switches event type from error to default', async () => {
  604. createWrapper({
  605. ruleId: rule.id,
  606. rule: {
  607. ...rule,
  608. eventTypes: ['error', 'default'],
  609. },
  610. });
  611. await userEvent.click(screen.getByText('event.type:error OR event.type:default'));
  612. await userEvent.click(await screen.findByText('event.type:default'));
  613. expect(screen.getAllByText('Number of Errors')).toHaveLength(2);
  614. await userEvent.click(screen.getByLabelText('Save Rule'));
  615. expect(editRule).toHaveBeenLastCalledWith(
  616. expect.anything(),
  617. expect.objectContaining({
  618. data: expect.objectContaining({
  619. eventTypes: ['default'],
  620. }),
  621. })
  622. );
  623. });
  624. it('saves a valid on demand metric rule', async () => {
  625. const validOnDemandMetricRule = MetricRuleFixture({
  626. query: 'transaction.duration:<1s',
  627. });
  628. const onSubmitSuccess = jest.fn();
  629. createWrapper({
  630. ruleId: validOnDemandMetricRule.id,
  631. rule: {
  632. ...validOnDemandMetricRule,
  633. eventTypes: ['transaction'],
  634. },
  635. onSubmitSuccess,
  636. });
  637. await userEvent.click(screen.getByLabelText('Save Rule'), {delay: null});
  638. expect(onSubmitSuccess).toHaveBeenCalled();
  639. });
  640. it('hides fields when migrating error metric alerts to filter archived issues', async () => {
  641. const errorAlert = MetricRuleFixture({
  642. dataset: Dataset.ERRORS,
  643. query: 'example-error',
  644. });
  645. location = {...location, query: {migration: '1'}};
  646. const onSubmitSuccess = jest.fn();
  647. createWrapper({
  648. ruleId: errorAlert.id,
  649. rule: {
  650. ...errorAlert,
  651. eventTypes: ['transaction'],
  652. },
  653. onSubmitSuccess,
  654. });
  655. expect(
  656. await screen.findByText(/please make sure the current thresholds are still valid/)
  657. ).toBeInTheDocument();
  658. await userEvent.click(screen.getByLabelText('Looks good to me!'), {delay: null});
  659. expect(onSubmitSuccess).toHaveBeenCalled();
  660. const formModel = onSubmitSuccess.mock.calls[0][1] as FormModel;
  661. expect(formModel.getData()).toEqual(
  662. expect.objectContaining({query: 'is:unresolved example-error'})
  663. );
  664. });
  665. });
  666. describe('Slack async lookup', () => {
  667. const uuid = 'xxxx-xxxx-xxxx';
  668. it('success status updates the rule', async () => {
  669. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  670. MockApiClient.addMockResponse({
  671. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  672. method: 'PUT',
  673. body: {uuid},
  674. statusCode: 202,
  675. });
  676. MockApiClient.addMockResponse({
  677. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  678. body: {
  679. status: 'success',
  680. alertRule,
  681. },
  682. });
  683. const onSubmitSuccess = jest.fn();
  684. createWrapper({
  685. ruleId: alertRule.id,
  686. rule: alertRule,
  687. onSubmitSuccess,
  688. });
  689. await screen.findByTestId('loading-indicator');
  690. await userEvent.type(
  691. await screen.findByPlaceholderText('Enter Alert Name'),
  692. 'Slack Alert Rule',
  693. {delay: null}
  694. );
  695. await userEvent.click(await screen.findByLabelText('Save Rule'), {delay: null});
  696. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  697. await waitFor(
  698. () => {
  699. expect(onSubmitSuccess).toHaveBeenCalledWith(
  700. expect.objectContaining({
  701. id: alertRule.id,
  702. name: alertRule.name,
  703. }),
  704. expect.anything()
  705. );
  706. },
  707. {timeout: 2000, interval: 10}
  708. );
  709. });
  710. it('pending status keeps loading true', async () => {
  711. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  712. MockApiClient.addMockResponse({
  713. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  714. method: 'PUT',
  715. body: {uuid},
  716. statusCode: 202,
  717. });
  718. MockApiClient.addMockResponse({
  719. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  720. body: {
  721. status: 'pending',
  722. },
  723. });
  724. const onSubmitSuccess = jest.fn();
  725. createWrapper({
  726. ruleId: alertRule.id,
  727. rule: alertRule,
  728. onSubmitSuccess,
  729. });
  730. expect(await screen.findByTestId('loading-indicator')).toBeInTheDocument();
  731. expect(onSubmitSuccess).not.toHaveBeenCalled();
  732. });
  733. it('failed status renders error message', async () => {
  734. const alertRule = MetricRuleFixture({name: 'Slack Alert Rule'});
  735. MockApiClient.addMockResponse({
  736. url: `/organizations/org-slug/alert-rules/${alertRule.id}/`,
  737. method: 'PUT',
  738. body: {uuid},
  739. statusCode: 202,
  740. });
  741. MockApiClient.addMockResponse({
  742. url: `/projects/org-slug/project-slug/alert-rule-task/${uuid}/`,
  743. body: {
  744. status: 'failed',
  745. error: 'An error occurred',
  746. },
  747. });
  748. const onSubmitSuccess = jest.fn();
  749. createWrapper({
  750. ruleId: alertRule.id,
  751. rule: alertRule,
  752. onSubmitSuccess,
  753. });
  754. await userEvent.type(
  755. await screen.findByPlaceholderText('Enter Alert Name'),
  756. 'Slack Alert Rule',
  757. {delay: null}
  758. );
  759. await userEvent.click(await screen.findByLabelText('Save Rule'), {delay: null});
  760. await waitFor(
  761. () => {
  762. expect(addErrorMessage).toHaveBeenCalledWith('An error occurred');
  763. },
  764. {timeout: 2000, interval: 10}
  765. );
  766. expect(onSubmitSuccess).not.toHaveBeenCalled();
  767. });
  768. });
  769. });