results.spec.tsx 39 KB

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