widgetQueries.spec.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import React from 'react';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {Client} from 'app/api';
  5. import WidgetQueries from 'app/views/dashboardsV2/widgetQueries';
  6. describe('Dashboards > WidgetQueries', function () {
  7. const initialData = initializeOrg({
  8. organization: TestStubs.Organization(),
  9. });
  10. const multipleQueryWidget = {
  11. title: 'Errors',
  12. interval: '5m',
  13. displayType: 'line',
  14. queries: [
  15. {conditions: 'event.type:error', fields: ['count()'], name: 'errors'},
  16. {conditions: 'event.type:default', fields: ['count()'], name: 'default'},
  17. ],
  18. };
  19. const singleQueryWidget = {
  20. title: 'Errors',
  21. interval: '5m',
  22. displayType: 'line',
  23. queries: [{conditions: 'event.type:error', fields: ['count()'], name: 'errors'}],
  24. };
  25. const tableWidget = {
  26. title: 'SDK',
  27. interval: '5m',
  28. displayType: 'table',
  29. queries: [{conditions: 'event.type:error', fields: ['sdk.name'], name: 'sdk'}],
  30. };
  31. const selection = {
  32. projects: [1],
  33. environments: ['prod'],
  34. datetime: {
  35. period: '14d',
  36. },
  37. };
  38. const api = new Client();
  39. afterEach(function () {
  40. MockApiClient.clearMockResponses();
  41. });
  42. it('can send multiple API requests', async function () {
  43. const errorMock = MockApiClient.addMockResponse(
  44. {
  45. url: '/organizations/org-slug/events-stats/',
  46. body: [],
  47. },
  48. {
  49. predicate(_url, options) {
  50. return (
  51. options.query.query === 'event.type:error' ||
  52. options.query.query === 'event.type:default'
  53. );
  54. },
  55. }
  56. );
  57. const wrapper = mountWithTheme(
  58. <WidgetQueries
  59. api={api}
  60. widget={multipleQueryWidget}
  61. organization={initialData.organization}
  62. selection={selection}
  63. >
  64. {() => <div data-test-id="child" />}
  65. </WidgetQueries>,
  66. initialData.routerContext
  67. );
  68. await tick();
  69. await tick();
  70. // Child should be rendered and 2 requests should be sent.
  71. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  72. expect(errorMock).toHaveBeenCalledTimes(2);
  73. });
  74. it('sets errorMessage when the first request fails', async function () {
  75. const okMock = MockApiClient.addMockResponse(
  76. {
  77. url: '/organizations/org-slug/events-stats/',
  78. body: [],
  79. },
  80. {
  81. predicate(_url, options) {
  82. return options.query.query === 'event.type:error';
  83. },
  84. }
  85. );
  86. const failMock = MockApiClient.addMockResponse(
  87. {
  88. url: '/organizations/org-slug/events-stats/',
  89. statusCode: 400,
  90. body: {detail: 'Bad request data'},
  91. },
  92. {
  93. predicate(_url, options) {
  94. return options.query.query === 'event.type:default';
  95. },
  96. }
  97. );
  98. let error = '';
  99. const wrapper = mountWithTheme(
  100. <WidgetQueries
  101. api={api}
  102. widget={multipleQueryWidget}
  103. organization={initialData.organization}
  104. selection={selection}
  105. >
  106. {({errorMessage}) => {
  107. error = errorMessage;
  108. return <div data-test-id="child" />;
  109. }}
  110. </WidgetQueries>,
  111. initialData.routerContext
  112. );
  113. await tick();
  114. await tick();
  115. // Child should be rendered and 2 requests should be sent.
  116. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  117. expect(okMock).toHaveBeenCalledTimes(1);
  118. expect(failMock).toHaveBeenCalledTimes(1);
  119. expect(error).toEqual('Bad request data');
  120. });
  121. it('adjusts interval based on date window', async function () {
  122. const errorMock = MockApiClient.addMockResponse({
  123. url: '/organizations/org-slug/events-stats/',
  124. body: [],
  125. });
  126. const widget = {...singleQueryWidget, interval: '1m'};
  127. const longSelection = {
  128. projects: [1],
  129. environments: ['prod', 'dev'],
  130. datetime: {
  131. period: '90d',
  132. },
  133. };
  134. const wrapper = mountWithTheme(
  135. <WidgetQueries
  136. api={api}
  137. widget={widget}
  138. organization={initialData.organization}
  139. selection={longSelection}
  140. >
  141. {() => <div data-test-id="child" />}
  142. </WidgetQueries>,
  143. initialData.routerContext
  144. );
  145. await tick();
  146. // Child should be rendered and interval bumped up.
  147. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  148. expect(errorMock).toHaveBeenCalledTimes(1);
  149. expect(errorMock).toHaveBeenCalledWith(
  150. '/organizations/org-slug/events-stats/',
  151. expect.objectContaining({
  152. query: expect.objectContaining({
  153. interval: '4h',
  154. statsPeriod: '90d',
  155. environment: ['prod', 'dev'],
  156. project: [1],
  157. }),
  158. })
  159. );
  160. });
  161. it('adjusts interval based on date window 14d', async function () {
  162. const errorMock = MockApiClient.addMockResponse({
  163. url: '/organizations/org-slug/events-stats/',
  164. body: [],
  165. });
  166. const widget = {...singleQueryWidget, interval: '1m'};
  167. const wrapper = mountWithTheme(
  168. <WidgetQueries
  169. api={api}
  170. widget={widget}
  171. organization={initialData.organization}
  172. selection={selection}
  173. >
  174. {() => <div data-test-id="child" />}
  175. </WidgetQueries>,
  176. initialData.routerContext
  177. );
  178. await tick();
  179. // Child should be rendered and interval bumped up.
  180. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  181. expect(errorMock).toHaveBeenCalledTimes(1);
  182. expect(errorMock).toHaveBeenCalledWith(
  183. '/organizations/org-slug/events-stats/',
  184. expect.objectContaining({
  185. query: expect.objectContaining({interval: '30m'}),
  186. })
  187. );
  188. });
  189. it('can send table result queries', async function () {
  190. const tableMock = MockApiClient.addMockResponse({
  191. url: '/organizations/org-slug/eventsv2/',
  192. body: {
  193. meta: {'sdk.name': 'string'},
  194. data: [{'sdk.name': 'python'}],
  195. },
  196. });
  197. let childProps = undefined;
  198. const wrapper = mountWithTheme(
  199. <WidgetQueries
  200. api={api}
  201. widget={tableWidget}
  202. organization={initialData.organization}
  203. selection={selection}
  204. >
  205. {props => {
  206. childProps = props;
  207. return <div data-test-id="child" />;
  208. }}
  209. </WidgetQueries>,
  210. initialData.routerContext
  211. );
  212. await tick();
  213. await tick();
  214. // Child should be rendered and 1 requests should be sent.
  215. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  216. expect(tableMock).toHaveBeenCalledTimes(1);
  217. expect(tableMock).toHaveBeenCalledWith(
  218. '/organizations/org-slug/eventsv2/',
  219. expect.objectContaining({
  220. query: expect.objectContaining({
  221. query: 'event.type:error',
  222. name: 'SDK',
  223. field: ['sdk.name'],
  224. statsPeriod: '14d',
  225. environment: ['prod'],
  226. project: [1],
  227. }),
  228. })
  229. );
  230. expect(childProps.timeseriesResults).toBeUndefined();
  231. expect(childProps.tableResults[0].data).toHaveLength(1);
  232. expect(childProps.tableResults[0].meta).toBeDefined();
  233. });
  234. it('can send multiple table queries', async function () {
  235. const firstQuery = MockApiClient.addMockResponse(
  236. {
  237. url: '/organizations/org-slug/eventsv2/',
  238. body: {
  239. meta: {'sdk.name': 'string'},
  240. data: [{'sdk.name': 'python'}],
  241. },
  242. },
  243. {
  244. predicate(_url, options) {
  245. return options.query.query === 'event.type:error';
  246. },
  247. }
  248. );
  249. const secondQuery = MockApiClient.addMockResponse(
  250. {
  251. url: '/organizations/org-slug/eventsv2/',
  252. body: {
  253. meta: {title: 'string'},
  254. data: [{title: 'ValueError'}],
  255. },
  256. },
  257. {
  258. predicate(_url, options) {
  259. return options.query.query === 'title:ValueError';
  260. },
  261. }
  262. );
  263. const widget = {
  264. title: 'SDK',
  265. interval: '5m',
  266. displayType: 'table',
  267. queries: [
  268. {conditions: 'event.type:error', fields: ['sdk.name'], name: 'sdk'},
  269. {conditions: 'title:ValueError', fields: ['title'], name: 'title'},
  270. ],
  271. };
  272. let childProps = undefined;
  273. const wrapper = mountWithTheme(
  274. <WidgetQueries
  275. api={api}
  276. widget={widget}
  277. organization={initialData.organization}
  278. selection={selection}
  279. >
  280. {props => {
  281. childProps = props;
  282. return <div data-test-id="child" />;
  283. }}
  284. </WidgetQueries>,
  285. initialData.routerContext
  286. );
  287. await tick();
  288. await tick();
  289. // Child should be rendered and 2 requests should be sent.
  290. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  291. expect(firstQuery).toHaveBeenCalledTimes(1);
  292. expect(secondQuery).toHaveBeenCalledTimes(1);
  293. expect(childProps.tableResults).toHaveLength(2);
  294. expect(childProps.tableResults[0].data[0]['sdk.name']).toBeDefined();
  295. expect(childProps.tableResults[1].data[0].title).toBeDefined();
  296. });
  297. it('can send big number result queries', async function () {
  298. const tableMock = MockApiClient.addMockResponse({
  299. url: '/organizations/org-slug/eventsv2/',
  300. body: {
  301. meta: {'sdk.name': 'string'},
  302. data: [{'sdk.name': 'python'}],
  303. },
  304. });
  305. let childProps = undefined;
  306. const wrapper = mountWithTheme(
  307. <WidgetQueries
  308. api={api}
  309. widget={{
  310. title: 'SDK',
  311. interval: '5m',
  312. displayType: 'big_number',
  313. queries: [{conditions: 'event.type:error', fields: ['sdk.name'], name: 'sdk'}],
  314. }}
  315. organization={initialData.organization}
  316. selection={selection}
  317. >
  318. {props => {
  319. childProps = props;
  320. return <div data-test-id="child" />;
  321. }}
  322. </WidgetQueries>,
  323. initialData.routerContext
  324. );
  325. await tick();
  326. await tick();
  327. // Child should be rendered and 1 requests should be sent.
  328. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  329. expect(tableMock).toHaveBeenCalledTimes(1);
  330. expect(tableMock).toHaveBeenCalledWith(
  331. '/organizations/org-slug/eventsv2/',
  332. expect.objectContaining({
  333. query: expect.objectContaining({
  334. referrer: 'api.dashboards.bignumberwidget',
  335. query: 'event.type:error',
  336. name: 'SDK',
  337. field: ['sdk.name'],
  338. statsPeriod: '14d',
  339. environment: ['prod'],
  340. project: [1],
  341. }),
  342. })
  343. );
  344. expect(childProps.timeseriesResults).toBeUndefined();
  345. expect(childProps.tableResults[0].data).toHaveLength(1);
  346. expect(childProps.tableResults[0].meta).toBeDefined();
  347. });
  348. it('can send world map result queries', async function () {
  349. const tableMock = MockApiClient.addMockResponse({
  350. url: '/organizations/org-slug/events-geo/',
  351. body: {
  352. meta: {'sdk.name': 'string'},
  353. data: [{'sdk.name': 'python'}],
  354. },
  355. });
  356. let childProps = undefined;
  357. const wrapper = mountWithTheme(
  358. <WidgetQueries
  359. api={api}
  360. widget={{
  361. title: 'SDK',
  362. interval: '5m',
  363. displayType: 'world_map',
  364. queries: [{conditions: 'event.type:error', fields: ['count()'], name: 'sdk'}],
  365. }}
  366. organization={initialData.organization}
  367. selection={selection}
  368. >
  369. {props => {
  370. childProps = props;
  371. return <div data-test-id="child" />;
  372. }}
  373. </WidgetQueries>,
  374. initialData.routerContext
  375. );
  376. await tick();
  377. await tick();
  378. // Child should be rendered and 1 requests should be sent.
  379. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  380. expect(tableMock).toHaveBeenCalledTimes(1);
  381. expect(tableMock).toHaveBeenCalledWith(
  382. '/organizations/org-slug/events-geo/',
  383. expect.objectContaining({
  384. query: expect.objectContaining({
  385. referrer: 'api.dashboards.worldmapwidget',
  386. query: 'event.type:error',
  387. name: 'SDK',
  388. field: ['count()'],
  389. statsPeriod: '14d',
  390. environment: ['prod'],
  391. project: [1],
  392. }),
  393. })
  394. );
  395. expect(childProps.timeseriesResults).toBeUndefined();
  396. expect(childProps.tableResults[0].data).toHaveLength(1);
  397. expect(childProps.tableResults[0].meta).toBeDefined();
  398. });
  399. it('stops loading state once all queries finish even if some fail', async function () {
  400. const firstQuery = MockApiClient.addMockResponse(
  401. {
  402. statusCode: 500,
  403. url: '/organizations/org-slug/eventsv2/',
  404. body: {detail: 'it didnt work'},
  405. },
  406. {
  407. predicate(_url, options) {
  408. return options.query.query === 'event.type:error';
  409. },
  410. }
  411. );
  412. const secondQuery = MockApiClient.addMockResponse(
  413. {
  414. url: '/organizations/org-slug/eventsv2/',
  415. body: {
  416. meta: {title: 'string'},
  417. data: [{title: 'ValueError'}],
  418. },
  419. },
  420. {
  421. predicate(_url, options) {
  422. return options.query.query === 'title:ValueError';
  423. },
  424. }
  425. );
  426. const widget = {
  427. title: 'SDK',
  428. interval: '5m',
  429. displayType: 'table',
  430. queries: [
  431. {conditions: 'event.type:error', fields: ['sdk.name'], name: 'sdk'},
  432. {conditions: 'title:ValueError', fields: ['title'], name: 'title'},
  433. ],
  434. };
  435. let childProps = undefined;
  436. const wrapper = mountWithTheme(
  437. <WidgetQueries
  438. api={api}
  439. widget={widget}
  440. organization={initialData.organization}
  441. selection={selection}
  442. >
  443. {props => {
  444. childProps = props;
  445. return <div data-test-id="child" />;
  446. }}
  447. </WidgetQueries>,
  448. initialData.routerContext
  449. );
  450. await tick();
  451. await tick();
  452. // Child should be rendered and 2 requests should be sent.
  453. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  454. expect(firstQuery).toHaveBeenCalledTimes(1);
  455. expect(secondQuery).toHaveBeenCalledTimes(1);
  456. expect(childProps.loading).toEqual(false);
  457. });
  458. it('sets bar charts to 1d interval', async function () {
  459. const errorMock = MockApiClient.addMockResponse(
  460. {
  461. url: '/organizations/org-slug/events-stats/',
  462. body: [],
  463. },
  464. {
  465. predicate(_url, options) {
  466. return options.query.interval === '1d';
  467. },
  468. }
  469. );
  470. const barWidget = {
  471. ...singleQueryWidget,
  472. displayType: 'bar',
  473. // Should be ignored for bars.
  474. interval: '5m',
  475. };
  476. const wrapper = mountWithTheme(
  477. <WidgetQueries
  478. api={api}
  479. widget={barWidget}
  480. organization={initialData.organization}
  481. selection={selection}
  482. >
  483. {() => <div data-test-id="child" />}
  484. </WidgetQueries>,
  485. initialData.routerContext
  486. );
  487. await tick();
  488. await tick();
  489. // Child should be rendered and 1 requests should be sent.
  490. expect(wrapper.find('[data-test-id="child"]')).toHaveLength(1);
  491. expect(errorMock).toHaveBeenCalledTimes(1);
  492. });
  493. });