groupEvents.spec.tsx 15 KB

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