results.spec.tsx 41 KB

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