groupEvents.spec.tsx 14 KB

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