groupEvents.spec.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. import type {Location} from 'history';
  2. import {GroupFixture} from 'sentry-fixture/group';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {
  5. render,
  6. screen,
  7. userEvent,
  8. waitFor,
  9. waitForElementToBeRemoved,
  10. } from 'sentry-test/reactTestingLibrary';
  11. import type {Group, Organization} from 'sentry/types';
  12. import {IssueCategory} from 'sentry/types/group';
  13. import {browserHistory} from 'sentry/utils/browserHistory';
  14. import GroupEvents from 'sentry/views/issueDetails/groupEvents';
  15. let location: Location;
  16. describe('groupEvents', () => {
  17. const requests: {[requestName: string]: jest.Mock} = {};
  18. const baseProps = Object.freeze({
  19. params: {orgId: 'orgId', groupId: '1'},
  20. route: {},
  21. routeParams: {},
  22. router: {} as any,
  23. routes: [],
  24. location: {},
  25. environments: [],
  26. group: GroupFixture() as Group,
  27. });
  28. let organization: Organization;
  29. let router;
  30. beforeEach(() => {
  31. browserHistory.push = jest.fn();
  32. ({organization, router} = initializeOrg({
  33. organization: {
  34. features: ['event-attachments'],
  35. },
  36. } as any));
  37. location = {
  38. pathname: '/organizations/org-slug/issues/123/events/',
  39. search: '',
  40. hash: '',
  41. action: 'REPLACE',
  42. key: 'okjkey',
  43. state: '',
  44. query: {
  45. query: '',
  46. },
  47. };
  48. requests.discover = MockApiClient.addMockResponse({
  49. url: '/organizations/org-slug/events/',
  50. headers: {
  51. Link: `<https://sentry.io/api/0/issues/1/events/?limit=50&cursor=0:0:1>; rel="previous"; results="true"; cursor="0:0:1", <https://sentry.io/api/0/issues/1/events/?limit=50&cursor=0:200:0>; rel="next"; results="true"; cursor="0:200:0"`,
  52. },
  53. body: {
  54. data: [
  55. {
  56. timestamp: '2022-09-11T15:01:10+00:00',
  57. transaction: '/api',
  58. release: 'backend@1.2.3',
  59. 'transaction.duration': 1803,
  60. environment: 'prod',
  61. 'user.display': 'sentry@sentry.sentry',
  62. id: 'id123',
  63. trace: 'trace123',
  64. 'project.name': 'project123',
  65. },
  66. ],
  67. meta: {
  68. fields: {
  69. timestamp: 'date',
  70. transaction: 'string',
  71. release: 'string',
  72. 'transaction.duration': 'duration',
  73. environment: 'string',
  74. 'user.display': 'string',
  75. id: 'string',
  76. trace: 'string',
  77. 'project.name': 'string',
  78. },
  79. units: {
  80. timestamp: null,
  81. transaction: null,
  82. release: null,
  83. 'transaction.duration': 'millisecond',
  84. environment: null,
  85. 'user.display': null,
  86. id: null,
  87. trace: null,
  88. 'project.name': null,
  89. },
  90. isMetricsData: false,
  91. tips: {query: null, columns: null},
  92. },
  93. },
  94. });
  95. requests.attachments = MockApiClient.addMockResponse({
  96. url: '/api/0/issues/1/attachments/?per_page=50&types=event.minidump&event_id=id123',
  97. body: [],
  98. });
  99. requests.recentSearches = MockApiClient.addMockResponse({
  100. method: 'POST',
  101. url: '/organizations/org-slug/recent-searches/',
  102. body: [],
  103. });
  104. requests.latestEvent = MockApiClient.addMockResponse({
  105. method: 'GET',
  106. url: '/issues/1/events/latest/',
  107. body: {},
  108. });
  109. requests.tags = MockApiClient.addMockResponse({
  110. method: 'GET',
  111. url: '/organizations/org-slug/issues/1/tags/',
  112. body: [],
  113. });
  114. });
  115. afterEach(() => {
  116. MockApiClient.clearMockResponses();
  117. jest.clearAllMocks();
  118. });
  119. it('fetches and renders a table of events', async () => {
  120. render(<GroupEvents {...baseProps} location={{...location, query: {}}} />, {
  121. router,
  122. organization,
  123. });
  124. expect(await screen.findByText('id123')).toBeInTheDocument();
  125. // Transaction
  126. expect(screen.getByText('/api')).toBeInTheDocument();
  127. // Environment
  128. expect(screen.getByText('prod')).toBeInTheDocument();
  129. // Release
  130. expect(screen.getByText('1.2.3')).toBeInTheDocument();
  131. // User email
  132. expect(screen.getByText('sentry@sentry.sentry')).toBeInTheDocument();
  133. });
  134. it('handles search', async () => {
  135. render(<GroupEvents {...baseProps} location={{...location, query: {}}} />, {
  136. router,
  137. organization,
  138. });
  139. const list = [
  140. {searchTerm: '', expectedQuery: ''},
  141. {searchTerm: 'test', expectedQuery: 'test'},
  142. {searchTerm: 'environment:production test', expectedQuery: 'test'},
  143. ];
  144. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  145. const input = screen.getByPlaceholderText('Search for events, users, tags, and more');
  146. for (const item of list) {
  147. await userEvent.clear(input);
  148. await userEvent.paste(`${item.searchTerm}`);
  149. await userEvent.keyboard('[Enter>]');
  150. expect(browserHistory.push).toHaveBeenCalledWith(
  151. expect.objectContaining({
  152. query: {query: item.expectedQuery},
  153. })
  154. );
  155. }
  156. });
  157. it('pushes new query parameter when searching (issue-stream-search-query-builder)', async () => {
  158. render(<GroupEvents {...baseProps} location={{...location, query: {}}} />, {
  159. router,
  160. organization: {...organization, features: ['issue-stream-search-query-builder']},
  161. });
  162. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  163. const input = screen.getByPlaceholderText('Search events...');
  164. await userEvent.click(input);
  165. await userEvent.keyboard('foo');
  166. await userEvent.keyboard('{enter}');
  167. await waitFor(() => {
  168. expect(browserHistory.push).toHaveBeenCalledWith(
  169. expect.objectContaining({
  170. query: {query: 'foo'},
  171. })
  172. );
  173. });
  174. });
  175. it('displays event filters and tags (issue-stream-search-query-builder)', async () => {
  176. MockApiClient.addMockResponse({
  177. url: '/organizations/org-slug/issues/1/tags/',
  178. body: [{key: 'custom_tag', name: 'custom_tag', totalValues: 1}],
  179. });
  180. render(<GroupEvents {...baseProps} location={{...location, query: {}}} />, {
  181. router,
  182. organization: {...organization, features: ['issue-stream-search-query-builder']},
  183. });
  184. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  185. const input = screen.getByPlaceholderText('Search events...');
  186. await userEvent.click(input);
  187. expect(
  188. await screen.findByRole('button', {name: 'Event Filters'})
  189. ).toBeInTheDocument();
  190. expect(screen.getByRole('button', {name: 'Event Tags'})).toBeInTheDocument();
  191. // Should show custom_tag fetched from group tags
  192. expect(screen.getByRole('option', {name: 'custom_tag'})).toBeInTheDocument();
  193. // Should hardcoded event filters
  194. expect(screen.getByRole('option', {name: 'event.type'})).toBeInTheDocument();
  195. });
  196. it('handles environment filtering', async () => {
  197. render(
  198. <GroupEvents
  199. {...baseProps}
  200. location={{...location, query: {environment: ['prod', 'staging']}}}
  201. />,
  202. {router, organization}
  203. );
  204. await waitFor(() => {
  205. expect(screen.getByText('transaction')).toBeInTheDocument();
  206. });
  207. expect(requests.discover).toHaveBeenCalledWith(
  208. '/organizations/org-slug/events/',
  209. expect.objectContaining({
  210. query: expect.objectContaining({environment: ['prod', 'staging']}),
  211. })
  212. );
  213. });
  214. it('renders events table for performance issue', async () => {
  215. const group = GroupFixture();
  216. group.issueCategory = IssueCategory.PERFORMANCE;
  217. render(
  218. <GroupEvents
  219. {...baseProps}
  220. group={group}
  221. location={{...location, query: {environment: ['prod', 'staging']}}}
  222. />,
  223. {router, organization}
  224. );
  225. expect(requests.discover).toHaveBeenCalledWith(
  226. '/organizations/org-slug/events/',
  227. expect.objectContaining({
  228. query: expect.objectContaining({
  229. query: 'performance.issue_ids:1 event.type:transaction ',
  230. }),
  231. })
  232. );
  233. await waitFor(() => {
  234. expect(screen.getByText('transaction')).toBeInTheDocument();
  235. });
  236. });
  237. it('renders event and trace link correctly', async () => {
  238. const group = GroupFixture();
  239. group.issueCategory = IssueCategory.PERFORMANCE;
  240. render(
  241. <GroupEvents
  242. {...baseProps}
  243. group={group}
  244. location={{...location, query: {environment: ['prod', 'staging']}}}
  245. />,
  246. {router, organization}
  247. );
  248. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  249. const eventIdATag = screen.getByText('id123').closest('a');
  250. expect(eventIdATag).toHaveAttribute(
  251. 'href',
  252. '/organizations/org-slug/issues/1/events/id123/'
  253. );
  254. });
  255. it('does not make attachments request, async when feature not enabled', async () => {
  256. render(
  257. <GroupEvents
  258. {...baseProps}
  259. location={{...location, query: {environment: ['prod', 'staging']}}}
  260. />,
  261. {router, organization: {...organization, features: []}}
  262. );
  263. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  264. const attachmentsColumn = screen.queryByText('attachments');
  265. expect(attachmentsColumn).not.toBeInTheDocument();
  266. expect(requests.attachments).not.toHaveBeenCalled();
  267. });
  268. it('does not display attachments column with no attachments', async () => {
  269. render(
  270. <GroupEvents
  271. {...baseProps}
  272. location={{...location, query: {environment: ['prod', 'staging']}}}
  273. />,
  274. {router, organization}
  275. );
  276. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  277. const attachmentsColumn = screen.queryByText('attachments');
  278. expect(attachmentsColumn).not.toBeInTheDocument();
  279. expect(requests.attachments).toHaveBeenCalled();
  280. });
  281. it('does not display minidump column with no minidumps', async () => {
  282. render(
  283. <GroupEvents
  284. {...baseProps}
  285. location={{...location, query: {environment: ['prod', 'staging']}}}
  286. />,
  287. {router, organization}
  288. );
  289. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  290. const minidumpColumn = screen.queryByText('minidump');
  291. expect(minidumpColumn).not.toBeInTheDocument();
  292. });
  293. it('displays minidumps', async () => {
  294. requests.attachments = MockApiClient.addMockResponse({
  295. url: '/api/0/issues/1/attachments/?per_page=50&types=event.minidump&event_id=id123',
  296. body: [
  297. {
  298. id: 'id123',
  299. name: 'dc42a8b9-fc22-4de1-8a29-45b3006496d8.dmp',
  300. headers: {
  301. 'Content-Type': 'application/octet-stream',
  302. },
  303. mimetype: 'application/octet-stream',
  304. size: 1294340,
  305. sha1: '742127552a1191f71fcf6ba7bc5afa0a837350e2',
  306. dateCreated: '2022-09-28T09:04:38.659307Z',
  307. type: 'event.minidump',
  308. event_id: 'd54cb9246ee241ffbdb39bf7a9fafbb7',
  309. },
  310. ],
  311. });
  312. render(
  313. <GroupEvents
  314. {...baseProps}
  315. location={{...location, query: {environment: ['prod', 'staging']}}}
  316. />,
  317. {router, organization}
  318. );
  319. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  320. const minidumpColumn = screen.queryByText('minidump');
  321. expect(minidumpColumn).toBeInTheDocument();
  322. });
  323. it('does not display attachments but displays minidump', async () => {
  324. requests.attachments = MockApiClient.addMockResponse({
  325. url: '/api/0/issues/1/attachments/?per_page=50&types=event.minidump&event_id=id123',
  326. body: [
  327. {
  328. id: 'id123',
  329. name: 'dc42a8b9-fc22-4de1-8a29-45b3006496d8.dmp',
  330. headers: {
  331. 'Content-Type': 'application/octet-stream',
  332. },
  333. mimetype: 'application/octet-stream',
  334. size: 1294340,
  335. sha1: '742127552a1191f71fcf6ba7bc5afa0a837350e2',
  336. dateCreated: '2022-09-28T09:04:38.659307Z',
  337. type: 'event.minidump',
  338. event_id: 'd54cb9246ee241ffbdb39bf7a9fafbb7',
  339. },
  340. ],
  341. });
  342. render(
  343. <GroupEvents
  344. {...baseProps}
  345. location={{...location, query: {environment: ['prod', 'staging']}}}
  346. />,
  347. {router, organization}
  348. );
  349. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  350. const attachmentsColumn = screen.queryByText('attachments');
  351. const minidumpColumn = screen.queryByText('minidump');
  352. expect(attachmentsColumn).not.toBeInTheDocument();
  353. expect(minidumpColumn).toBeInTheDocument();
  354. expect(requests.attachments).toHaveBeenCalled();
  355. });
  356. it('renders events table for error', async () => {
  357. render(
  358. <GroupEvents
  359. {...baseProps}
  360. location={{...location, query: {environment: ['prod', 'staging']}}}
  361. />,
  362. {router, organization}
  363. );
  364. expect(requests.discover).toHaveBeenCalledWith(
  365. '/organizations/org-slug/events/',
  366. expect.objectContaining({
  367. query: expect.objectContaining({
  368. query: 'issue.id:1 ',
  369. field: expect.not.arrayContaining(['attachments', 'minidump']),
  370. }),
  371. })
  372. );
  373. await waitFor(() => {
  374. expect(screen.getByText('transaction')).toBeInTheDocument();
  375. });
  376. });
  377. it('removes sort if unsupported by the events table', async () => {
  378. render(
  379. <GroupEvents
  380. {...baseProps}
  381. location={{...location, query: {environment: ['prod', 'staging'], sort: 'user'}}}
  382. />,
  383. {router, organization}
  384. );
  385. expect(requests.discover).toHaveBeenCalledWith(
  386. '/organizations/org-slug/events/',
  387. expect.objectContaining({query: expect.not.objectContaining({sort: 'user'})})
  388. );
  389. await waitFor(() => {
  390. expect(screen.getByText('transaction')).toBeInTheDocument();
  391. });
  392. });
  393. it('only request for a single projectId', async () => {
  394. const group = GroupFixture();
  395. render(
  396. <GroupEvents
  397. {...baseProps}
  398. group={group}
  399. location={{
  400. ...location,
  401. query: {
  402. environment: ['prod', 'staging'],
  403. sort: 'user',
  404. project: [group.project.id, '456'],
  405. },
  406. }}
  407. />,
  408. {router, organization}
  409. );
  410. expect(requests.discover).toHaveBeenCalledWith(
  411. '/organizations/org-slug/events/',
  412. expect.objectContaining({
  413. query: expect.objectContaining({project: [group.project.id]}),
  414. })
  415. );
  416. await waitFor(() => {
  417. expect(screen.getByText('transaction')).toBeInTheDocument();
  418. });
  419. });
  420. it('shows discover query error message', async () => {
  421. requests.discover = MockApiClient.addMockResponse({
  422. url: '/organizations/org-slug/events/',
  423. statusCode: 500,
  424. body: {
  425. detail: 'Internal Error',
  426. errorId: '69ab396e73704cdba9342ff8dcd59795',
  427. },
  428. });
  429. render(
  430. <GroupEvents
  431. {...baseProps}
  432. location={{...location, query: {environment: ['prod', 'staging']}}}
  433. />,
  434. {router, organization}
  435. );
  436. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  437. expect(screen.getByTestId('loading-error')).toHaveTextContent('Internal Error');
  438. });
  439. it('requests for backend columns if backend project', async () => {
  440. const group = GroupFixture();
  441. group.project.platform = 'node-express';
  442. render(
  443. <GroupEvents
  444. {...baseProps}
  445. group={group}
  446. location={{...location, query: {environment: ['prod', 'staging']}}}
  447. />,
  448. {router, organization}
  449. );
  450. await waitForElementToBeRemoved(() => screen.queryByTestId('loading-indicator'));
  451. expect(requests.discover).toHaveBeenCalledWith(
  452. '/organizations/org-slug/events/',
  453. expect.objectContaining({
  454. query: expect.objectContaining({
  455. field: expect.arrayContaining(['url', 'runtime']),
  456. }),
  457. })
  458. );
  459. expect(requests.discover).toHaveBeenCalledWith(
  460. '/organizations/org-slug/events/',
  461. expect.objectContaining({
  462. query: expect.objectContaining({
  463. field: expect.not.arrayContaining(['browser']),
  464. }),
  465. })
  466. );
  467. });
  468. });