results.spec.tsx 41 KB

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