groupEvents.spec.tsx 14 KB

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