results.spec.tsx 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. import {browserHistory} from 'react-router';
  2. import selectEvent from 'react-select-event';
  3. import {Organization} from 'sentry-fixture/organization';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  6. import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence';
  7. import ProjectsStore from 'sentry/stores/projectsStore';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import Results from 'sentry/views/discover/results';
  10. import {DEFAULT_EVENT_VIEW, TRANSACTION_VIEWS} from './data';
  11. const FIELDS = [
  12. {
  13. field: 'title',
  14. },
  15. {
  16. field: 'timestamp',
  17. },
  18. {
  19. field: 'user',
  20. },
  21. {
  22. field: 'count()',
  23. },
  24. ];
  25. const generateFields = () => ({
  26. field: FIELDS.map(i => i.field),
  27. });
  28. const eventTitle = 'Oh no something bad';
  29. function renderMockRequests() {
  30. MockApiClient.addMockResponse({
  31. url: '/organizations/org-slug/projects/',
  32. body: [],
  33. });
  34. MockApiClient.addMockResponse({
  35. url: '/organizations/org-slug/projects-count/',
  36. body: {myProjects: 10, allProjects: 300},
  37. });
  38. MockApiClient.addMockResponse({
  39. url: '/organizations/org-slug/tags/',
  40. body: [],
  41. });
  42. const eventsStatsMock = MockApiClient.addMockResponse({
  43. url: '/organizations/org-slug/events-stats/',
  44. body: {data: [[123, []]]},
  45. });
  46. MockApiClient.addMockResponse({
  47. url: '/organizations/org-slug/recent-searches/',
  48. body: [],
  49. });
  50. MockApiClient.addMockResponse({
  51. url: '/organizations/org-slug/recent-searches/',
  52. method: 'POST',
  53. body: [],
  54. });
  55. MockApiClient.addMockResponse({
  56. url: '/organizations/org-slug/releases/stats/',
  57. body: [],
  58. });
  59. const measurementsMetaMock = MockApiClient.addMockResponse({
  60. url: '/organizations/org-slug/measurements-meta/',
  61. method: 'GET',
  62. body: {},
  63. });
  64. const eventsResultsMock = MockApiClient.addMockResponse({
  65. url: '/organizations/org-slug/events/',
  66. body: {
  67. meta: {
  68. fields: {
  69. id: 'string',
  70. title: 'string',
  71. 'project.name': 'string',
  72. timestamp: 'date',
  73. 'user.id': 'string',
  74. },
  75. },
  76. data: [
  77. {
  78. id: 'deadbeef',
  79. 'user.id': 'alberto leal',
  80. title: eventTitle,
  81. 'project.name': 'project-slug',
  82. timestamp: '2019-05-23T22:12:48+00:00',
  83. },
  84. ],
  85. },
  86. });
  87. const eventsMetaMock = MockApiClient.addMockResponse({
  88. url: '/organizations/org-slug/events-meta/',
  89. body: {
  90. count: 2,
  91. },
  92. });
  93. MockApiClient.addMockResponse({
  94. url: '/organizations/org-slug/events/project-slug:deadbeef/',
  95. method: 'GET',
  96. body: {
  97. id: '1234',
  98. size: 1200,
  99. eventID: 'deadbeef',
  100. title: 'Oh no something bad',
  101. message: 'It was not good',
  102. dateCreated: '2019-05-23T22:12:48+00:00',
  103. entries: [
  104. {
  105. type: 'message',
  106. message: 'bad stuff',
  107. data: {},
  108. },
  109. ],
  110. tags: [{key: 'browser', value: 'Firefox'}],
  111. },
  112. });
  113. const eventFacetsMock = MockApiClient.addMockResponse({
  114. url: '/organizations/org-slug/events-facets/',
  115. body: [
  116. {
  117. key: 'release',
  118. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  119. },
  120. {
  121. key: 'environment',
  122. topValues: [
  123. {count: 2, value: 'dev', name: 'dev'},
  124. {count: 1, value: 'prod', name: 'prod'},
  125. ],
  126. },
  127. {
  128. key: 'foo',
  129. topValues: [
  130. {count: 2, value: 'bar', name: 'bar'},
  131. {count: 1, value: 'baz', name: 'baz'},
  132. ],
  133. },
  134. ],
  135. });
  136. const mockVisit = MockApiClient.addMockResponse({
  137. url: '/organizations/org-slug/discover/saved/1/visit/',
  138. method: 'POST',
  139. body: [],
  140. statusCode: 200,
  141. });
  142. const mockSaved = MockApiClient.addMockResponse({
  143. url: '/organizations/org-slug/discover/saved/1/',
  144. method: 'GET',
  145. statusCode: 200,
  146. body: {
  147. id: '1',
  148. name: 'new',
  149. projects: [],
  150. version: 2,
  151. expired: false,
  152. dateCreated: '2021-04-08T17:53:25.195782Z',
  153. dateUpdated: '2021-04-09T12:13:18.567264Z',
  154. createdBy: {
  155. id: '2',
  156. },
  157. environment: [],
  158. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  159. widths: ['-1', '-1', '-1', '-1', '-1'],
  160. range: '24h',
  161. orderby: '-user.display',
  162. },
  163. });
  164. MockApiClient.addMockResponse({
  165. url: '/organizations/org-slug/discover/homepage/',
  166. method: 'GET',
  167. statusCode: 200,
  168. body: {
  169. id: '2',
  170. name: '',
  171. projects: [],
  172. version: 2,
  173. expired: false,
  174. dateCreated: '2021-04-08T17:53:25.195782Z',
  175. dateUpdated: '2021-04-09T12:13:18.567264Z',
  176. createdBy: {
  177. id: '2',
  178. },
  179. environment: [],
  180. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  181. widths: ['-1', '-1', '-1', '-1', '-1'],
  182. range: '24h',
  183. orderby: '-user.display',
  184. },
  185. });
  186. MockApiClient.addMockResponse({
  187. url: '/organizations/org-slug/dynamic-sampling/custom-rules/',
  188. method: 'GET',
  189. statusCode: 204,
  190. body: '',
  191. });
  192. return {
  193. eventsStatsMock,
  194. eventsMetaMock,
  195. eventsResultsMock,
  196. mockVisit,
  197. mockSaved,
  198. eventFacetsMock,
  199. measurementsMetaMock,
  200. };
  201. }
  202. describe('Results', function () {
  203. afterEach(function () {
  204. MockApiClient.clearMockResponses();
  205. ProjectsStore.reset();
  206. });
  207. describe('Events', function () {
  208. const features = ['discover-basic'];
  209. it('loads data when moving from an invalid to valid EventView', function () {
  210. const organization = Organization({
  211. features,
  212. });
  213. // Start off with an invalid view (empty is invalid)
  214. const initialData = initializeOrg({
  215. organization,
  216. router: {
  217. location: {query: {query: 'tag:value'}},
  218. },
  219. });
  220. const mockRequests = renderMockRequests();
  221. ProjectsStore.loadInitialData([TestStubs.Project()]);
  222. render(
  223. <Results
  224. location={initialData.router.location}
  225. router={initialData.router}
  226. loading={false}
  227. setSavedQuery={jest.fn()}
  228. />,
  229. {
  230. context: initialData.routerContext,
  231. organization,
  232. }
  233. );
  234. // No request as eventview was invalid.
  235. expect(mockRequests.eventsStatsMock).not.toHaveBeenCalled();
  236. // Should redirect and retain the old query value
  237. expect(browserHistory.replace).toHaveBeenCalledWith(
  238. expect.objectContaining({
  239. pathname: '/organizations/org-slug/discover/results/',
  240. query: expect.objectContaining({
  241. query: 'tag:value',
  242. }),
  243. })
  244. );
  245. });
  246. it('pagination cursor should be cleared when making a search', async function () {
  247. const organization = Organization({
  248. features,
  249. });
  250. const initialData = initializeOrg({
  251. organization,
  252. router: {
  253. location: {
  254. query: {
  255. ...generateFields(),
  256. cursor: '0%3A50%3A0',
  257. },
  258. },
  259. },
  260. });
  261. const mockRequests = renderMockRequests();
  262. ProjectsStore.loadInitialData([TestStubs.Project()]);
  263. render(
  264. <Results
  265. organization={organization}
  266. location={initialData.router.location}
  267. router={initialData.router}
  268. loading={false}
  269. setSavedQuery={jest.fn()}
  270. />,
  271. {
  272. context: initialData.routerContext,
  273. organization,
  274. }
  275. );
  276. // ensure cursor query string is initially present in the location
  277. expect(initialData.router.location).toEqual({
  278. query: {
  279. ...generateFields(),
  280. cursor: '0%3A50%3A0',
  281. },
  282. });
  283. await waitFor(() =>
  284. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument()
  285. );
  286. // perform a search
  287. await userEvent.click(
  288. screen.getByPlaceholderText('Search for events, users, tags, and more')
  289. );
  290. await userEvent.paste('geo:canada');
  291. await userEvent.keyboard('{enter}');
  292. // should only be called with saved queries
  293. expect(mockRequests.mockVisit).not.toHaveBeenCalled();
  294. // cursor query string should be omitted from the query string
  295. expect(initialData.router.push).toHaveBeenCalledWith({
  296. pathname: undefined,
  297. query: {
  298. ...generateFields(),
  299. query: 'geo:canada',
  300. statsPeriod: '14d',
  301. },
  302. });
  303. });
  304. it('renders a y-axis selector', async function () {
  305. const organization = Organization({
  306. features,
  307. });
  308. const initialData = initializeOrg({
  309. organization,
  310. router: {
  311. location: {query: {...generateFields(), yAxis: 'count()'}},
  312. },
  313. });
  314. renderMockRequests();
  315. ProjectsStore.loadInitialData([TestStubs.Project()]);
  316. render(
  317. <Results
  318. organization={organization}
  319. location={initialData.router.location}
  320. router={initialData.router}
  321. loading={false}
  322. setSavedQuery={jest.fn()}
  323. />,
  324. {
  325. context: initialData.routerContext,
  326. organization,
  327. }
  328. );
  329. // Click the 'default' option.
  330. await selectEvent.select(
  331. await screen.findByRole('button', {name: 'Y-Axis count()'}),
  332. 'count_unique(user)'
  333. );
  334. });
  335. it('renders a display selector', async function () {
  336. const organization = Organization({
  337. features,
  338. });
  339. const initialData = initializeOrg({
  340. organization,
  341. router: {
  342. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  343. },
  344. });
  345. renderMockRequests();
  346. ProjectsStore.loadInitialData([TestStubs.Project()]);
  347. render(
  348. <Results
  349. organization={organization}
  350. location={initialData.router.location}
  351. router={initialData.router}
  352. loading={false}
  353. setSavedQuery={jest.fn()}
  354. />,
  355. {
  356. context: initialData.routerContext,
  357. organization,
  358. }
  359. );
  360. // Click the 'default' option.
  361. await selectEvent.select(
  362. await screen.findByRole('button', {name: /Display/}),
  363. 'Total Period'
  364. );
  365. });
  366. it('excludes top5 options when plan does not include discover-query', async function () {
  367. const organization = Organization({
  368. features: ['discover-basic'],
  369. });
  370. const initialData = initializeOrg({
  371. organization,
  372. router: {
  373. location: {query: {...generateFields(), display: 'previous'}},
  374. },
  375. });
  376. ProjectsStore.loadInitialData([TestStubs.Project()]);
  377. renderMockRequests();
  378. render(
  379. <Results
  380. organization={organization}
  381. location={initialData.router.location}
  382. router={initialData.router}
  383. loading={false}
  384. setSavedQuery={jest.fn()}
  385. />,
  386. {
  387. context: initialData.routerContext,
  388. organization,
  389. }
  390. );
  391. await userEvent.click(await screen.findByRole('button', {name: /Display/}));
  392. expect(screen.queryByText('Top 5 Daily')).not.toBeInTheDocument();
  393. expect(screen.queryByText('Top 5 Period')).not.toBeInTheDocument();
  394. });
  395. it('needs confirmation on long queries', async function () {
  396. const organization = Organization({
  397. features: ['discover-basic'],
  398. });
  399. const initialData = initializeOrg({
  400. organization,
  401. router: {
  402. location: {query: {...generateFields(), statsPeriod: '60d', project: '-1'}},
  403. },
  404. });
  405. ProjectsStore.loadInitialData([TestStubs.Project()]);
  406. const mockRequests = renderMockRequests();
  407. render(
  408. <Results
  409. organization={organization}
  410. location={initialData.router.location}
  411. router={initialData.router}
  412. loading={false}
  413. setSavedQuery={jest.fn()}
  414. />,
  415. {
  416. context: initialData.routerContext,
  417. organization,
  418. }
  419. );
  420. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(0);
  421. await waitFor(() => {
  422. expect(mockRequests.measurementsMetaMock).toHaveBeenCalled();
  423. });
  424. });
  425. it('needs confirmation on long query with explicit projects', async function () {
  426. const organization = Organization({
  427. features: ['discover-basic'],
  428. });
  429. const initialData = initializeOrg({
  430. organization,
  431. router: {
  432. location: {
  433. query: {
  434. ...generateFields(),
  435. statsPeriod: '60d',
  436. project: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(String),
  437. },
  438. },
  439. },
  440. });
  441. ProjectsStore.loadInitialData([TestStubs.Project()]);
  442. const mockRequests = renderMockRequests();
  443. render(
  444. <Results
  445. organization={organization}
  446. location={initialData.router.location}
  447. router={initialData.router}
  448. loading={false}
  449. setSavedQuery={jest.fn()}
  450. />,
  451. {
  452. context: initialData.routerContext,
  453. organization,
  454. }
  455. );
  456. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(0);
  457. await waitFor(() => {
  458. expect(mockRequests.measurementsMetaMock).toHaveBeenCalled();
  459. });
  460. });
  461. it('does not need confirmation on short queries', async function () {
  462. const organization = Organization({
  463. features: ['discover-basic'],
  464. });
  465. const initialData = initializeOrg({
  466. organization,
  467. router: {
  468. location: {query: {...generateFields(), statsPeriod: '30d', project: '-1'}},
  469. },
  470. });
  471. ProjectsStore.loadInitialData([TestStubs.Project()]);
  472. const mockRequests = renderMockRequests();
  473. render(
  474. <Results
  475. organization={organization}
  476. location={initialData.router.location}
  477. router={initialData.router}
  478. loading={false}
  479. setSavedQuery={jest.fn()}
  480. />,
  481. {
  482. context: initialData.routerContext,
  483. organization,
  484. }
  485. );
  486. await waitFor(() => {
  487. expect(mockRequests.measurementsMetaMock).toHaveBeenCalled();
  488. });
  489. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(1);
  490. });
  491. it('does not need confirmation with to few projects', async function () {
  492. const organization = Organization({
  493. features: ['discover-basic'],
  494. });
  495. const initialData = initializeOrg({
  496. organization,
  497. router: {
  498. location: {
  499. query: {
  500. ...generateFields(),
  501. statsPeriod: '90d',
  502. project: [1, 2, 3, 4].map(String),
  503. },
  504. },
  505. },
  506. });
  507. ProjectsStore.loadInitialData([TestStubs.Project()]);
  508. const mockRequests = renderMockRequests();
  509. render(
  510. <Results
  511. organization={organization}
  512. location={initialData.router.location}
  513. router={initialData.router}
  514. loading={false}
  515. setSavedQuery={jest.fn()}
  516. />,
  517. {
  518. context: initialData.routerContext,
  519. organization,
  520. }
  521. );
  522. await waitFor(() => {
  523. expect(mockRequests.measurementsMetaMock).toHaveBeenCalled();
  524. });
  525. expect(mockRequests.eventsResultsMock).toHaveBeenCalledTimes(1);
  526. });
  527. it('creates event view from saved query', async function () {
  528. const organization = Organization({
  529. features,
  530. slug: 'org-slug',
  531. });
  532. const initialData = initializeOrg({
  533. organization,
  534. router: {
  535. location: {query: {id: '1', statsPeriod: '24h'}},
  536. },
  537. });
  538. ProjectsStore.loadInitialData([TestStubs.Project()]);
  539. const mockRequests = renderMockRequests();
  540. render(
  541. <Results
  542. organization={organization}
  543. location={initialData.router.location}
  544. router={initialData.router}
  545. loading={false}
  546. setSavedQuery={jest.fn()}
  547. />,
  548. {
  549. context: initialData.routerContext,
  550. organization,
  551. }
  552. );
  553. await waitFor(() => expect(mockRequests.mockVisit).toHaveBeenCalled());
  554. expect(screen.getByRole('link', {name: 'timestamp'})).toHaveAttribute(
  555. 'href',
  556. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-timestamp&statsPeriod=24h&topEvents=5'
  557. );
  558. expect(screen.getByRole('link', {name: 'project'})).toHaveAttribute(
  559. 'href',
  560. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-project&statsPeriod=24h&topEvents=5'
  561. );
  562. expect(screen.getByRole('link', {name: 'deadbeef'})).toHaveAttribute(
  563. 'href',
  564. '/organizations/org-slug/discover/project-slug:deadbeef/?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-user.display&statsPeriod=24h&topEvents=5&yAxis=count%28%29'
  565. );
  566. expect(screen.getByRole('link', {name: 'user.display'})).toHaveAttribute(
  567. 'href',
  568. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=user.display&statsPeriod=24h&topEvents=5'
  569. );
  570. expect(screen.getByRole('link', {name: 'title'})).toHaveAttribute(
  571. 'href',
  572. 'undefined?field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&query=&sort=-title&statsPeriod=24h&topEvents=5'
  573. );
  574. });
  575. it('overrides saved query params with location query params', async function () {
  576. const organization = Organization({
  577. features,
  578. slug: 'org-slug',
  579. });
  580. const initialData = initializeOrg({
  581. organization,
  582. router: {
  583. location: {
  584. query: {
  585. id: '1',
  586. statsPeriod: '7d',
  587. project: ['2'],
  588. environment: ['production'],
  589. },
  590. },
  591. },
  592. });
  593. ProjectsStore.loadInitialData([TestStubs.Project()]);
  594. const mockRequests = renderMockRequests();
  595. render(
  596. <Results
  597. organization={organization}
  598. location={initialData.router.location}
  599. router={initialData.router}
  600. loading={false}
  601. setSavedQuery={jest.fn()}
  602. />,
  603. {
  604. context: initialData.routerContext,
  605. organization,
  606. }
  607. );
  608. await waitFor(() => expect(mockRequests.mockVisit).toHaveBeenCalled());
  609. expect(screen.getByRole('link', {name: 'timestamp'})).toHaveAttribute(
  610. 'href',
  611. 'undefined?environment=production&field=title&field=event.type&field=project&field=user.display&field=timestamp&id=1&name=new&project=2&query=&sort=-timestamp&statsPeriod=7d&topEvents=5'
  612. );
  613. });
  614. it('updates chart whenever yAxis parameter changes', async function () {
  615. const organization = Organization({
  616. features,
  617. });
  618. const initialData = initializeOrg({
  619. organization,
  620. router: {
  621. location: {query: {...generateFields(), yAxis: 'count()'}},
  622. },
  623. });
  624. ProjectsStore.loadInitialData([TestStubs.Project()]);
  625. const {eventsStatsMock, measurementsMetaMock} = renderMockRequests();
  626. const {rerender} = render(
  627. <Results
  628. organization={organization}
  629. location={initialData.router.location}
  630. router={initialData.router}
  631. loading={false}
  632. setSavedQuery={jest.fn()}
  633. />,
  634. {
  635. context: initialData.routerContext,
  636. organization,
  637. }
  638. );
  639. // Should load events once
  640. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  641. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  642. 1,
  643. '/organizations/org-slug/events-stats/',
  644. expect.objectContaining({
  645. query: expect.objectContaining({
  646. statsPeriod: '14d',
  647. yAxis: ['count()'],
  648. }),
  649. })
  650. );
  651. await waitFor(() => {
  652. expect(measurementsMetaMock).toHaveBeenCalled();
  653. });
  654. // Update location simulating a browser back button action
  655. rerender(
  656. <Results
  657. organization={organization}
  658. location={{
  659. ...initialData.router.location,
  660. query: {...generateFields(), yAxis: 'count_unique(user)'},
  661. }}
  662. router={initialData.router}
  663. loading={false}
  664. setSavedQuery={jest.fn()}
  665. />
  666. );
  667. // Should load events again
  668. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  669. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  670. 2,
  671. '/organizations/org-slug/events-stats/',
  672. expect.objectContaining({
  673. query: expect.objectContaining({
  674. statsPeriod: '14d',
  675. yAxis: ['count_unique(user)'],
  676. }),
  677. })
  678. );
  679. await waitFor(() => {
  680. expect(measurementsMetaMock).toHaveBeenCalled();
  681. });
  682. });
  683. it('updates chart whenever display parameter changes', async function () {
  684. const organization = Organization({
  685. features,
  686. });
  687. const initialData = initializeOrg({
  688. organization,
  689. router: {
  690. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  691. },
  692. });
  693. const {eventsStatsMock, measurementsMetaMock} = renderMockRequests();
  694. ProjectsStore.loadInitialData([TestStubs.Project()]);
  695. const {rerender} = render(
  696. <Results
  697. organization={organization}
  698. location={initialData.router.location}
  699. router={initialData.router}
  700. loading={false}
  701. setSavedQuery={jest.fn()}
  702. />,
  703. {
  704. context: initialData.routerContext,
  705. organization,
  706. }
  707. );
  708. // Should load events once
  709. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  710. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  711. 1,
  712. '/organizations/org-slug/events-stats/',
  713. expect.objectContaining({
  714. query: expect.objectContaining({
  715. statsPeriod: '14d',
  716. yAxis: ['count()'],
  717. }),
  718. })
  719. );
  720. await waitFor(() => {
  721. expect(measurementsMetaMock).toHaveBeenCalled();
  722. });
  723. // Update location simulating a browser back button action
  724. rerender(
  725. <Results
  726. organization={organization}
  727. location={{
  728. ...initialData.router.location,
  729. query: {...generateFields(), display: 'previous', yAxis: 'count()'},
  730. }}
  731. router={initialData.router}
  732. loading={false}
  733. setSavedQuery={jest.fn()}
  734. />
  735. );
  736. // Should load events again
  737. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  738. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  739. 2,
  740. '/organizations/org-slug/events-stats/',
  741. expect.objectContaining({
  742. query: expect.objectContaining({
  743. statsPeriod: '28d',
  744. yAxis: ['count()'],
  745. }),
  746. })
  747. );
  748. await waitFor(() => {
  749. expect(measurementsMetaMock).toHaveBeenCalled();
  750. });
  751. });
  752. it('updates chart whenever display and yAxis parameters change', async function () {
  753. const organization = Organization({
  754. features,
  755. });
  756. const initialData = initializeOrg({
  757. organization,
  758. router: {
  759. location: {query: {...generateFields(), display: 'default', yAxis: 'count()'}},
  760. },
  761. });
  762. const {eventsStatsMock, measurementsMetaMock} = renderMockRequests();
  763. ProjectsStore.loadInitialData([TestStubs.Project()]);
  764. const {rerender} = render(
  765. <Results
  766. organization={organization}
  767. location={initialData.router.location}
  768. router={initialData.router}
  769. loading={false}
  770. setSavedQuery={jest.fn()}
  771. />,
  772. {
  773. context: initialData.routerContext,
  774. organization,
  775. }
  776. );
  777. // Should load events once
  778. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  779. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  780. 1,
  781. '/organizations/org-slug/events-stats/',
  782. expect.objectContaining({
  783. query: expect.objectContaining({
  784. statsPeriod: '14d',
  785. yAxis: ['count()'],
  786. }),
  787. })
  788. );
  789. await waitFor(() => {
  790. expect(measurementsMetaMock).toHaveBeenCalled();
  791. });
  792. // Update location simulating a browser back button action
  793. rerender(
  794. <Results
  795. organization={organization}
  796. location={{
  797. ...initialData.router.location,
  798. query: {
  799. ...generateFields(),
  800. display: 'previous',
  801. yAxis: 'count_unique(user)',
  802. },
  803. }}
  804. router={initialData.router}
  805. loading={false}
  806. setSavedQuery={jest.fn()}
  807. />
  808. );
  809. // Should load events again
  810. expect(eventsStatsMock).toHaveBeenCalledTimes(2);
  811. expect(eventsStatsMock).toHaveBeenNthCalledWith(
  812. 2,
  813. '/organizations/org-slug/events-stats/',
  814. expect.objectContaining({
  815. query: expect.objectContaining({
  816. statsPeriod: '28d',
  817. yAxis: ['count_unique(user)'],
  818. }),
  819. })
  820. );
  821. await waitFor(() => {
  822. expect(measurementsMetaMock).toHaveBeenCalled();
  823. });
  824. });
  825. it('appends tag value to existing query when clicked', async function () {
  826. const organization = Organization({
  827. features,
  828. });
  829. const initialData = initializeOrg({
  830. organization,
  831. router: {
  832. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  833. },
  834. });
  835. const mockRequests = renderMockRequests();
  836. ProjectsStore.loadInitialData([TestStubs.Project()]);
  837. render(
  838. <Results
  839. organization={organization}
  840. location={initialData.router.location}
  841. router={initialData.router}
  842. loading={false}
  843. setSavedQuery={jest.fn()}
  844. />,
  845. {
  846. context: initialData.routerContext,
  847. organization,
  848. }
  849. );
  850. await userEvent.click(await screen.findByRole('button', {name: 'Show Tags'}));
  851. await waitFor(() => expect(mockRequests.eventFacetsMock).toHaveBeenCalled());
  852. // TODO(edward): update this to be less generic
  853. await userEvent.click(screen.getByText('environment'));
  854. await userEvent.click(screen.getByText('foo'));
  855. // since environment collides with the environment field, it is wrapped with `tags[...]`
  856. expect(
  857. await screen.findByRole('link', {
  858. name: 'environment, dev, 100% of all events. View events with this tag value.',
  859. })
  860. ).toBeInTheDocument();
  861. expect(
  862. screen.getByRole('link', {
  863. name: 'foo, bar, 100% of all events. View events with this tag value.',
  864. })
  865. ).toBeInTheDocument();
  866. });
  867. it('respects pinned filters for prebuilt queries', async function () {
  868. const organization = Organization({
  869. features: [...features, 'global-views'],
  870. });
  871. const initialData = initializeOrg({
  872. organization,
  873. router: {
  874. location: {query: {...generateFields(), display: 'default', yAxis: 'count'}},
  875. },
  876. });
  877. renderMockRequests();
  878. jest.spyOn(PageFilterPersistence, 'getPageFilterStorage').mockReturnValue({
  879. state: {
  880. project: [1],
  881. environment: [],
  882. start: null,
  883. end: null,
  884. period: '14d',
  885. utc: null,
  886. },
  887. pinnedFilters: new Set(['projects']),
  888. });
  889. ProjectsStore.loadInitialData([TestStubs.Project({id: 1, slug: 'Pinned Project'})]);
  890. render(
  891. <Results
  892. organization={organization}
  893. location={initialData.router.location}
  894. router={initialData.router}
  895. loading={false}
  896. setSavedQuery={jest.fn()}
  897. />,
  898. {context: initialData.routerContext, organization}
  899. );
  900. const projectPageFilter = await screen.findByTestId('page-filter-project-selector');
  901. expect(projectPageFilter).toHaveTextContent('All Projects');
  902. });
  903. it('displays tip when events response contains a tip', async function () {
  904. renderMockRequests();
  905. MockApiClient.addMockResponse({
  906. url: '/organizations/org-slug/events/',
  907. body: {
  908. meta: {
  909. fields: {},
  910. tips: {query: 'this is a tip'},
  911. },
  912. data: [],
  913. },
  914. });
  915. const organization = Organization({
  916. features,
  917. });
  918. const initialData = initializeOrg({
  919. organization,
  920. router: {
  921. location: {query: {...generateFields(), yAxis: 'count()'}},
  922. },
  923. });
  924. ProjectsStore.loadInitialData([TestStubs.Project()]);
  925. render(
  926. <Results
  927. organization={organization}
  928. location={initialData.router.location}
  929. router={initialData.router}
  930. loading={false}
  931. setSavedQuery={jest.fn()}
  932. />,
  933. {context: initialData.routerContext, organization}
  934. );
  935. expect(await screen.findByText('this is a tip')).toBeInTheDocument();
  936. });
  937. it('renders metric fallback alert', async function () {
  938. const organization = Organization({
  939. features: ['discover-basic'],
  940. });
  941. const initialData = initializeOrg({
  942. organization,
  943. router: {
  944. location: {query: {fromMetric: 'true', id: '1'}},
  945. },
  946. });
  947. ProjectsStore.loadInitialData([TestStubs.Project()]);
  948. renderMockRequests();
  949. render(
  950. <Results
  951. organization={organization}
  952. location={initialData.router.location}
  953. router={initialData.router}
  954. loading={false}
  955. setSavedQuery={jest.fn()}
  956. />,
  957. {
  958. context: initialData.routerContext,
  959. organization,
  960. }
  961. );
  962. expect(
  963. await screen.findByText(
  964. /You've navigated to this page from a performance metric widget generated from processed events/
  965. )
  966. ).toBeInTheDocument();
  967. });
  968. it('renders unparameterized data banner', async function () {
  969. const organization = Organization({
  970. features: ['discover-basic'],
  971. });
  972. const initialData = initializeOrg({
  973. organization,
  974. router: {
  975. location: {query: {showUnparameterizedBanner: 'true', id: '1'}},
  976. },
  977. });
  978. ProjectsStore.loadInitialData([TestStubs.Project()]);
  979. renderMockRequests();
  980. render(
  981. <Results
  982. organization={organization}
  983. location={initialData.router.location}
  984. router={initialData.router}
  985. loading={false}
  986. setSavedQuery={jest.fn()}
  987. />,
  988. {
  989. context: initialData.routerContext,
  990. organization,
  991. }
  992. );
  993. expect(
  994. await screen.findByText(/These are unparameterized transactions/)
  995. ).toBeInTheDocument();
  996. });
  997. it('updates the homepage query with up to date eventView when Set as Default is clicked', async () => {
  998. const mockHomepageUpdate = MockApiClient.addMockResponse({
  999. url: '/organizations/org-slug/discover/homepage/',
  1000. method: 'PUT',
  1001. statusCode: 200,
  1002. });
  1003. const organization = Organization({
  1004. features: ['discover-basic', 'discover-query'],
  1005. });
  1006. const initialData = initializeOrg({
  1007. organization,
  1008. router: {
  1009. // These fields take priority and should be sent in the request
  1010. location: {query: {field: ['title', 'user'], id: '1'}},
  1011. },
  1012. });
  1013. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1014. renderMockRequests();
  1015. render(
  1016. <Results
  1017. organization={organization}
  1018. location={initialData.router.location}
  1019. router={initialData.router}
  1020. loading={false}
  1021. setSavedQuery={jest.fn()}
  1022. />,
  1023. {context: initialData.routerContext, organization}
  1024. );
  1025. await waitFor(() =>
  1026. expect(screen.getByRole('button', {name: /set as default/i})).toBeEnabled()
  1027. );
  1028. await userEvent.click(screen.getByText('Set as Default'));
  1029. expect(mockHomepageUpdate).toHaveBeenCalledWith(
  1030. '/organizations/org-slug/discover/homepage/',
  1031. expect.objectContaining({
  1032. data: expect.objectContaining({
  1033. fields: ['title', 'user'],
  1034. }),
  1035. })
  1036. );
  1037. });
  1038. it('Changes the Use as Discover button to a reset button for saved query', async () => {
  1039. renderMockRequests();
  1040. MockApiClient.addMockResponse({
  1041. url: '/organizations/org-slug/discover/homepage/',
  1042. method: 'PUT',
  1043. statusCode: 200,
  1044. body: {
  1045. id: '2',
  1046. name: '',
  1047. projects: [],
  1048. version: 2,
  1049. expired: false,
  1050. dateCreated: '2021-04-08T17:53:25.195782Z',
  1051. dateUpdated: '2021-04-09T12:13:18.567264Z',
  1052. createdBy: {
  1053. id: '2',
  1054. },
  1055. environment: [],
  1056. fields: ['title', 'event.type', 'project', 'user.display', 'timestamp'],
  1057. widths: ['-1', '-1', '-1', '-1', '-1'],
  1058. range: '24h',
  1059. orderby: '-user.display',
  1060. },
  1061. });
  1062. const organization = Organization({
  1063. features: ['discover-basic', 'discover-query'],
  1064. });
  1065. const initialData = initializeOrg({
  1066. organization,
  1067. router: {
  1068. location: {query: {id: '1'}},
  1069. },
  1070. });
  1071. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1072. renderMockRequests();
  1073. const {rerender} = render(
  1074. <Results
  1075. loading={false}
  1076. setSavedQuery={jest.fn()}
  1077. organization={organization}
  1078. location={initialData.router.location}
  1079. router={initialData.router}
  1080. />,
  1081. {context: initialData.routerContext, organization}
  1082. );
  1083. await waitFor(() =>
  1084. expect(screen.getByRole('button', {name: /set as default/i})).toBeEnabled()
  1085. );
  1086. await userEvent.click(screen.getByText('Set as Default'));
  1087. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  1088. await userEvent.click(screen.getByText('Total Period'));
  1089. await userEvent.click(screen.getByText('Previous Period'));
  1090. const rerenderData = initializeOrg({
  1091. organization,
  1092. router: {
  1093. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1094. },
  1095. });
  1096. rerender(
  1097. <Results
  1098. loading={false}
  1099. setSavedQuery={jest.fn()}
  1100. organization={organization}
  1101. location={rerenderData.router.location}
  1102. router={rerenderData.router}
  1103. />
  1104. );
  1105. screen.getByText('Previous Period');
  1106. expect(await screen.findByText('Set as Default')).toBeInTheDocument();
  1107. });
  1108. it('Changes the Use as Discover button to a reset button for prebuilt query', async () => {
  1109. MockApiClient.addMockResponse({
  1110. url: '/organizations/org-slug/discover/homepage/',
  1111. method: 'PUT',
  1112. statusCode: 200,
  1113. body: {...TRANSACTION_VIEWS[0], name: ''},
  1114. });
  1115. const organization = Organization({
  1116. features: ['discover-basic', 'discover-query'],
  1117. });
  1118. const initialData = initializeOrg({
  1119. organization,
  1120. router: {
  1121. location: {
  1122. ...TestStubs.location(),
  1123. query: {
  1124. ...EventView.fromNewQueryWithLocation(
  1125. TRANSACTION_VIEWS[0],
  1126. TestStubs.location()
  1127. ).generateQueryStringObject(),
  1128. },
  1129. },
  1130. },
  1131. });
  1132. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1133. renderMockRequests();
  1134. const {rerender} = render(
  1135. <Results
  1136. organization={organization}
  1137. location={initialData.router.location}
  1138. router={initialData.router}
  1139. loading={false}
  1140. setSavedQuery={jest.fn()}
  1141. />,
  1142. {context: initialData.routerContext, organization}
  1143. );
  1144. await screen.findAllByText(TRANSACTION_VIEWS[0].name);
  1145. await userEvent.click(screen.getByText('Set as Default'));
  1146. expect(await screen.findByText('Remove Default')).toBeInTheDocument();
  1147. await userEvent.click(screen.getByText('Total Period'));
  1148. await userEvent.click(screen.getByText('Previous Period'));
  1149. const rerenderData = initializeOrg({
  1150. organization,
  1151. router: {
  1152. location: {query: {...initialData.router.location.query, display: 'previous'}},
  1153. },
  1154. });
  1155. rerender(
  1156. <Results
  1157. organization={organization}
  1158. location={rerenderData.router.location}
  1159. router={rerenderData.router}
  1160. loading={false}
  1161. setSavedQuery={jest.fn()}
  1162. />
  1163. );
  1164. screen.getByText('Previous Period');
  1165. expect(await screen.findByText('Set as Default')).toBeInTheDocument();
  1166. });
  1167. it('links back to the homepage through the Discover breadcrumb', async () => {
  1168. const organization = Organization({
  1169. features: ['discover-basic', 'discover-query'],
  1170. });
  1171. const initialData = initializeOrg({
  1172. organization,
  1173. router: {
  1174. location: {query: {id: '1'}},
  1175. },
  1176. });
  1177. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1178. const {measurementsMetaMock} = renderMockRequests();
  1179. render(
  1180. <Results
  1181. organization={organization}
  1182. location={initialData.router.location}
  1183. router={initialData.router}
  1184. loading={false}
  1185. setSavedQuery={jest.fn()}
  1186. />,
  1187. {context: initialData.routerContext, organization}
  1188. );
  1189. await waitFor(() => {
  1190. expect(measurementsMetaMock).toHaveBeenCalled();
  1191. });
  1192. expect(screen.getByText('Discover')).toHaveAttribute(
  1193. 'href',
  1194. expect.stringMatching(new RegExp('^/organizations/org-slug/discover/homepage/'))
  1195. );
  1196. });
  1197. it('links back to the Saved Queries through the Saved Queries breadcrumb', async () => {
  1198. const organization = Organization({
  1199. features: ['discover-basic', 'discover-query'],
  1200. });
  1201. const initialData = initializeOrg({
  1202. organization,
  1203. router: {
  1204. location: {query: {id: '1'}},
  1205. },
  1206. });
  1207. const {measurementsMetaMock} = renderMockRequests();
  1208. render(
  1209. <Results
  1210. organization={organization}
  1211. location={initialData.router.location}
  1212. router={initialData.router}
  1213. loading={false}
  1214. setSavedQuery={jest.fn()}
  1215. />,
  1216. {context: initialData.routerContext, organization}
  1217. );
  1218. await waitFor(() => {
  1219. expect(measurementsMetaMock).toHaveBeenCalled();
  1220. });
  1221. expect(screen.getByRole('link', {name: 'Saved Queries'})).toHaveAttribute(
  1222. 'href',
  1223. expect.stringMatching(new RegExp('^/organizations/org-slug/discover/queries/'))
  1224. );
  1225. });
  1226. it('allows users to Set As Default on the All Events query', async () => {
  1227. const organization = Organization({
  1228. features: ['discover-basic', 'discover-query'],
  1229. });
  1230. const initialData = initializeOrg({
  1231. organization,
  1232. router: {
  1233. location: {
  1234. ...TestStubs.location(),
  1235. query: {
  1236. ...EventView.fromNewQueryWithLocation(
  1237. DEFAULT_EVENT_VIEW,
  1238. TestStubs.location()
  1239. ).generateQueryStringObject(),
  1240. },
  1241. },
  1242. },
  1243. });
  1244. ProjectsStore.loadInitialData([TestStubs.Project()]);
  1245. const {measurementsMetaMock} = renderMockRequests();
  1246. render(
  1247. <Results
  1248. organization={organization}
  1249. location={initialData.router.location}
  1250. router={initialData.router}
  1251. loading={false}
  1252. setSavedQuery={jest.fn()}
  1253. />,
  1254. {context: initialData.routerContext, organization}
  1255. );
  1256. await waitFor(() => {
  1257. expect(measurementsMetaMock).toHaveBeenCalled();
  1258. });
  1259. expect(screen.getByTestId('set-as-default')).toBeEnabled();
  1260. });
  1261. it("doesn't render sample data alert", async function () {
  1262. const organization = Organization({
  1263. features: ['discover-basic', 'discover-query'],
  1264. });
  1265. const initialData = initializeOrg({
  1266. organization,
  1267. router: {
  1268. location: {
  1269. ...TestStubs.location(),
  1270. query: {
  1271. ...EventView.fromNewQueryWithLocation(
  1272. {...DEFAULT_EVENT_VIEW, query: 'event.type:error'},
  1273. TestStubs.location()
  1274. ).generateQueryStringObject(),
  1275. },
  1276. },
  1277. },
  1278. });
  1279. const {measurementsMetaMock} = renderMockRequests();
  1280. render(
  1281. <Results
  1282. organization={organization}
  1283. location={initialData.router.location}
  1284. router={initialData.router}
  1285. loading={false}
  1286. setSavedQuery={jest.fn()}
  1287. />,
  1288. {context: initialData.routerContext, organization}
  1289. );
  1290. await waitFor(() => {
  1291. expect(measurementsMetaMock).toHaveBeenCalled();
  1292. });
  1293. expect(screen.queryByText(/Based on your search criteria/)).not.toBeInTheDocument();
  1294. });
  1295. });
  1296. });