results.spec.tsx 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505
  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. const eventsResultsMock = 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. await waitFor(() => {
  936. expect(eventsResultsMock).toHaveBeenCalled();
  937. });
  938. expect(screen.getByText('this is a tip')).toBeInTheDocument();
  939. });
  940. it('renders metric fallback alert', async function () {
  941. const organization = Organization({
  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([TestStubs.Project()]);
  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 = Organization({
  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([TestStubs.Project()]);
  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 = Organization({
  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([TestStubs.Project()]);
  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 = Organization({
  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([TestStubs.Project()]);
  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 = Organization({
  1119. features: ['discover-basic', 'discover-query'],
  1120. });
  1121. const initialData = initializeOrg({
  1122. organization,
  1123. router: {
  1124. location: {
  1125. ...TestStubs.location(),
  1126. query: {
  1127. ...EventView.fromNewQueryWithLocation(
  1128. TRANSACTION_VIEWS[0],
  1129. TestStubs.location()
  1130. ).generateQueryStringObject(),
  1131. },
  1132. },
  1133. },
  1134. });
  1135. ProjectsStore.loadInitialData([TestStubs.Project()]);
  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 = Organization({
  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([TestStubs.Project()]);
  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 = Organization({
  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 = Organization({
  1231. features: ['discover-basic', 'discover-query'],
  1232. });
  1233. const initialData = initializeOrg({
  1234. organization,
  1235. router: {
  1236. location: {
  1237. ...TestStubs.location(),
  1238. query: {
  1239. ...EventView.fromNewQueryWithLocation(
  1240. DEFAULT_EVENT_VIEW,
  1241. TestStubs.location()
  1242. ).generateQueryStringObject(),
  1243. },
  1244. },
  1245. },
  1246. });
  1247. ProjectsStore.loadInitialData([TestStubs.Project()]);
  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 = Organization({
  1266. features: ['discover-basic', 'discover-query'],
  1267. });
  1268. const initialData = initializeOrg({
  1269. organization,
  1270. router: {
  1271. location: {
  1272. ...TestStubs.location(),
  1273. query: {
  1274. ...EventView.fromNewQueryWithLocation(
  1275. {...DEFAULT_EVENT_VIEW, query: 'event.type:error'},
  1276. TestStubs.location()
  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. });