results.spec.tsx 41 KB

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