results.spec.tsx 39 KB

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