groupEvents.spec.tsx 14 KB

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