groupEvents.spec.tsx 14 KB

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