groupEventDetails.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. import type {InjectedRouter} from 'react-router';
  2. import {browserHistory} from 'react-router';
  3. import type {Location} from 'history';
  4. import {CommitFixture} from 'sentry-fixture/commit';
  5. import {CommitAuthorFixture} from 'sentry-fixture/commitAuthor';
  6. import {EventFixture} from 'sentry-fixture/event';
  7. import {GroupFixture} from 'sentry-fixture/group';
  8. import {LocationFixture} from 'sentry-fixture/locationFixture';
  9. import {ProjectFixture} from 'sentry-fixture/project';
  10. import {RouterContextFixture} from 'sentry-fixture/routerContextFixture';
  11. import {RouterFixture} from 'sentry-fixture/routerFixture';
  12. import {SentryAppFixture} from 'sentry-fixture/sentryApp';
  13. import {SentryAppComponentFixture} from 'sentry-fixture/sentryAppComponent';
  14. import {SentryAppInstallationFixture} from 'sentry-fixture/sentryAppInstallation';
  15. import {initializeOrg} from 'sentry-test/initializeOrg';
  16. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  17. import type {Event, Group} from 'sentry/types';
  18. import {EntryType, IssueCategory, IssueType} from 'sentry/types';
  19. import type {Organization} from 'sentry/types/organization';
  20. import type {Project} from 'sentry/types/project';
  21. import type {QuickTraceEvent} from 'sentry/utils/performance/quickTrace/types';
  22. import type {GroupEventDetailsProps} from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
  23. import GroupEventDetails from 'sentry/views/issueDetails/groupEventDetails/groupEventDetails';
  24. import {ReprocessingStatus} from 'sentry/views/issueDetails/utils';
  25. import {RouteContext} from 'sentry/views/routeContext';
  26. const TRACE_ID = '797cda4e24844bdc90e0efe741616047';
  27. const makeDefaultMockData = (
  28. organization?: Organization,
  29. project?: Project,
  30. environments?: string[]
  31. ): {
  32. event: Event;
  33. group: Group;
  34. organization: Organization;
  35. project: Project;
  36. router: InjectedRouter;
  37. } => {
  38. return {
  39. organization: organization ?? initializeOrg().organization,
  40. project: project ?? initializeOrg().project,
  41. group: GroupFixture(),
  42. router: RouterFixture({
  43. location: LocationFixture({
  44. query: {
  45. environment: environments,
  46. },
  47. }),
  48. }),
  49. event: EventFixture({
  50. size: 1,
  51. dateCreated: '2019-03-20T00:00:00.000Z',
  52. errors: [],
  53. entries: [],
  54. tags: [
  55. {key: 'environment', value: 'dev'},
  56. {key: 'mechanism', value: 'ANR'},
  57. ],
  58. contexts: {
  59. trace: {
  60. trace_id: TRACE_ID,
  61. span_id: 'b0e6f15b45c36b12',
  62. op: 'ui.action.click',
  63. type: 'trace',
  64. },
  65. },
  66. }),
  67. };
  68. };
  69. function TestComponent(
  70. props: Partial<GroupEventDetailsProps> & {environments?: string[]}
  71. ) {
  72. const {organization, project, group, event, router} = makeDefaultMockData(
  73. props.organization,
  74. props.project,
  75. props.environments ?? ['dev']
  76. );
  77. const mergedProps: GroupEventDetailsProps = {
  78. group,
  79. event,
  80. project,
  81. organization,
  82. params: {groupId: group.id, eventId: '1'},
  83. router,
  84. location: {} as Location<any>,
  85. route: {},
  86. eventError: props.eventError ?? false,
  87. groupReprocessingStatus:
  88. props.groupReprocessingStatus ?? ReprocessingStatus.NO_STATUS,
  89. onRetry: props?.onRetry ?? jest.fn(),
  90. loadingEvent: props.loadingEvent ?? false,
  91. routes: [],
  92. routeParams: {},
  93. ...props,
  94. };
  95. return (
  96. <RouteContext.Provider
  97. value={{
  98. router,
  99. location: router.location,
  100. params: router.params,
  101. routes: router.routes,
  102. }}
  103. >
  104. <GroupEventDetails {...mergedProps} />;
  105. </RouteContext.Provider>
  106. );
  107. }
  108. const mockedTrace = (project: Project) => {
  109. return {
  110. event_id: '8806ea4691c24fc7b1c77ecd78df574f',
  111. span_id: 'b0e6f15b45c36b12',
  112. transaction: 'MainActivity.add_attachment',
  113. 'transaction.duration': 1000,
  114. 'transaction.op': 'navigation',
  115. project_id: parseInt(project.id, 10),
  116. project_slug: project.slug,
  117. parent_span_id: null,
  118. parent_event_id: null,
  119. generation: 0,
  120. errors: [
  121. {
  122. event_id: 'c6971a73454646338bc3ec80c70f8891',
  123. issue_id: 104,
  124. span: 'b0e6f15b45c36b12',
  125. project_id: parseInt(project.id, 10),
  126. project_slug: project.slug,
  127. title: 'ApplicationNotResponding: ANR for at least 5000 ms.',
  128. level: 'error',
  129. issue: '',
  130. },
  131. ],
  132. performance_issues: [
  133. {
  134. event_id: '8806ea4691c24fc7b1c77ecd78df574f',
  135. issue_id: 110,
  136. issue_short_id: 'SENTRY-ANDROID-1R',
  137. span: ['b0e6f15b45c36b12'],
  138. suspect_spans: ['89930aab9a0314d4'],
  139. project_id: parseInt(project.id, 10),
  140. project_slug: project.slug,
  141. title: 'File IO on Main Thread',
  142. level: 'info',
  143. culprit: 'MainActivity.add_attachment',
  144. type: 1008,
  145. end: 1678290375.15056,
  146. start: 1678290374.150562,
  147. },
  148. ],
  149. timestamp: 1678290375.150561,
  150. start_timestamp: 1678290374.150561,
  151. children: [],
  152. } as QuickTraceEvent;
  153. };
  154. const mockGroupApis = (
  155. organization: Organization,
  156. project: Project,
  157. group: Group,
  158. event: Event,
  159. trace?: QuickTraceEvent
  160. ) => {
  161. MockApiClient.addMockResponse({
  162. url: `/organizations/${organization.slug}/issues/${group.id}/`,
  163. body: group,
  164. });
  165. MockApiClient.addMockResponse({
  166. url: `/projects/${organization.slug}/${project.slug}/issues/`,
  167. method: 'PUT',
  168. });
  169. MockApiClient.addMockResponse({
  170. url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/committers/`,
  171. body: {committers: []},
  172. });
  173. MockApiClient.addMockResponse({
  174. url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/owners/`,
  175. body: {owners: [], rules: []},
  176. });
  177. MockApiClient.addMockResponse({
  178. url: `/organizations/${organization.slug}/issues/${group.id}/tags/`,
  179. body: [],
  180. });
  181. MockApiClient.addMockResponse({
  182. url: `/organizations/${organization.slug}/events-trace/${TRACE_ID}/`,
  183. body: trace
  184. ? {transactions: [trace], orphan_errors: []}
  185. : {transactions: [], orphan_errors: []},
  186. });
  187. MockApiClient.addMockResponse({
  188. url: `/organizations/${organization.slug}/events-trace-light/${TRACE_ID}/`,
  189. body: trace
  190. ? {transactions: [trace], orphan_errors: []}
  191. : {transactions: [], orphan_errors: []},
  192. });
  193. MockApiClient.addMockResponse({
  194. url: `/organizations/${organization.slug}/issues/${group.id}/integrations/`,
  195. body: [],
  196. });
  197. MockApiClient.addMockResponse({
  198. url: `/organizations/${organization.slug}/issues/${group.id}/external-issues/`,
  199. });
  200. MockApiClient.addMockResponse({
  201. url: `/organizations/${organization.slug}/issues/${group.id}/current-release/`,
  202. body: {currentRelease: null},
  203. });
  204. MockApiClient.addMockResponse({
  205. url: `/organizations/${organization.slug}/prompts-activity/`,
  206. body: undefined,
  207. });
  208. MockApiClient.addMockResponse({
  209. url: `/organizations/${organization.slug}/has-mobile-app-events/`,
  210. body: null,
  211. });
  212. MockApiClient.addMockResponse({
  213. url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/grouping-info/`,
  214. body: {},
  215. });
  216. MockApiClient.addMockResponse({
  217. url: `/projects/${organization.slug}/${project.slug}/codeowners/`,
  218. body: [],
  219. });
  220. MockApiClient.addMockResponse({
  221. url: `/organizations/${organization.slug}/code-mappings/`,
  222. method: 'GET',
  223. body: [],
  224. });
  225. MockApiClient.addMockResponse({
  226. url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/actionable-items/`,
  227. body: {
  228. errors: [],
  229. },
  230. });
  231. // Sentry related mocks
  232. MockApiClient.addMockResponse({
  233. url: '/sentry-apps/',
  234. body: [],
  235. });
  236. MockApiClient.addMockResponse({
  237. url: `/organizations/${organization.slug}/sentry-apps/`,
  238. body: [],
  239. });
  240. MockApiClient.addMockResponse({
  241. url: `/organizations/${organization.slug}/sentry-app-installations/`,
  242. body: [],
  243. });
  244. MockApiClient.addMockResponse({
  245. url: `/organizations/${organization.slug}/sentry-app-components/`,
  246. body: [],
  247. match: [MockApiClient.matchQuery({projectId: project.id})],
  248. });
  249. MockApiClient.addMockResponse({
  250. url: '/projects/org-slug/project-slug/',
  251. body: project,
  252. });
  253. MockApiClient.addMockResponse({
  254. url: '/organizations/org-slug/users/',
  255. body: [],
  256. });
  257. MockApiClient.addMockResponse({
  258. url: '/organizations/org-slug/projects/',
  259. body: [project],
  260. });
  261. MockApiClient.addMockResponse({
  262. url: `/customers/org-slug/policies/`,
  263. body: {},
  264. });
  265. MockApiClient.addMockResponse({
  266. url: `/organizations/${organization.slug}/issues/${group.id}/first-last-release/`,
  267. method: 'GET',
  268. });
  269. MockApiClient.addMockResponse({
  270. url: `/organizations/${organization.slug}/events/`,
  271. body: {
  272. data: [],
  273. meta: {fields: {}, units: {}},
  274. },
  275. });
  276. };
  277. describe('groupEventDetails', () => {
  278. beforeEach(() => {
  279. MockApiClient.clearMockResponses();
  280. });
  281. afterEach(function () {
  282. MockApiClient.clearMockResponses();
  283. (browserHistory.replace as jest.Mock).mockClear();
  284. });
  285. it('redirects on switching to an invalid environment selection for event', async function () {
  286. const props = makeDefaultMockData();
  287. mockGroupApis(props.organization, props.project, props.group, props.event);
  288. const {rerender} = render(<TestComponent {...props} />, {
  289. organization: props.organization,
  290. });
  291. expect(browserHistory.replace).not.toHaveBeenCalled();
  292. rerender(<TestComponent environments={['prod']} />);
  293. await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled());
  294. });
  295. it('does not redirect when switching to a valid environment selection for event', async function () {
  296. const props = makeDefaultMockData();
  297. mockGroupApis(props.organization, props.project, props.group, props.event);
  298. const {rerender} = render(<TestComponent {...props} />, {
  299. organization: props.organization,
  300. });
  301. expect(browserHistory.replace).not.toHaveBeenCalled();
  302. rerender(<TestComponent environments={[]} />);
  303. expect(await screen.findByTestId('group-event-details')).toBeInTheDocument();
  304. expect(browserHistory.replace).not.toHaveBeenCalled();
  305. });
  306. it('displays error on event error', async function () {
  307. const props = makeDefaultMockData();
  308. mockGroupApis(
  309. props.organization,
  310. props.project,
  311. props.group,
  312. EventFixture({
  313. size: 1,
  314. dateCreated: '2019-03-20T00:00:00.000Z',
  315. errors: [],
  316. entries: [],
  317. tags: [{key: 'environment', value: 'dev'}],
  318. previousEventID: 'prev-event-id',
  319. nextEventID: 'next-event-id',
  320. })
  321. );
  322. render(<TestComponent event={undefined} eventError />, {
  323. organization: props.organization,
  324. });
  325. expect(
  326. await screen.findByText(/events for this issue could not be found/)
  327. ).toBeInTheDocument();
  328. });
  329. it('renders the Span Evidence and Resources section for Performance Issues', async function () {
  330. const props = makeDefaultMockData();
  331. const group: Group = GroupFixture({
  332. issueCategory: IssueCategory.PERFORMANCE,
  333. issueType: IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  334. });
  335. const transaction = EventFixture({
  336. entries: [{type: EntryType.SPANS, data: []}],
  337. });
  338. mockGroupApis(
  339. props.organization,
  340. props.project,
  341. props.group,
  342. EventFixture({
  343. size: 1,
  344. dateCreated: '2019-03-20T00:00:00.000Z',
  345. errors: [],
  346. entries: [],
  347. tags: [{key: 'environment', value: 'dev'}],
  348. previousEventID: 'prev-event-id',
  349. nextEventID: 'next-event-id',
  350. })
  351. );
  352. const routerContext = RouterContextFixture();
  353. render(<TestComponent group={group} event={transaction} />, {
  354. organization: props.organization,
  355. context: routerContext,
  356. });
  357. expect(
  358. await screen.findByRole('heading', {
  359. name: /span evidence/i,
  360. })
  361. ).toBeInTheDocument();
  362. expect(
  363. screen.getByRole('heading', {
  364. name: /resources/i,
  365. })
  366. ).toBeInTheDocument();
  367. });
  368. it('renders the Function Evidence and Resources section for Profile Issues', async function () {
  369. const props = makeDefaultMockData();
  370. const group: Group = GroupFixture({
  371. issueCategory: IssueCategory.PERFORMANCE,
  372. issueType: IssueType.PROFILE_FILE_IO_MAIN_THREAD,
  373. });
  374. const transaction = EventFixture({
  375. entries: [],
  376. occurrence: {
  377. evidenceDisplay: [],
  378. evidenceData: {
  379. templateName: 'profile',
  380. },
  381. type: 2001,
  382. },
  383. });
  384. mockGroupApis(
  385. props.organization,
  386. props.project,
  387. props.group,
  388. EventFixture({
  389. size: 1,
  390. dateCreated: '2019-03-20T00:00:00.000Z',
  391. errors: [],
  392. entries: [],
  393. tags: [{key: 'environment', value: 'dev'}],
  394. previousEventID: 'prev-event-id',
  395. nextEventID: 'next-event-id',
  396. })
  397. );
  398. const routerContext = RouterContextFixture();
  399. render(<TestComponent group={group} event={transaction} />, {
  400. organization: props.organization,
  401. context: routerContext,
  402. });
  403. expect(
  404. await screen.findByRole('heading', {
  405. name: /function evidence/i,
  406. })
  407. ).toBeInTheDocument();
  408. expect(
  409. screen.getByRole('heading', {
  410. name: /resources/i,
  411. })
  412. ).toBeInTheDocument();
  413. });
  414. });
  415. describe('EventCause', () => {
  416. beforeEach(() => {
  417. MockApiClient.clearMockResponses();
  418. });
  419. afterEach(function () {
  420. MockApiClient.clearMockResponses();
  421. (browserHistory.replace as jest.Mock).mockClear();
  422. });
  423. it('renders suspect commit', async function () {
  424. const props = makeDefaultMockData(
  425. undefined,
  426. ProjectFixture({firstEvent: EventFixture().dateCreated})
  427. );
  428. mockGroupApis(
  429. props.organization,
  430. props.project,
  431. props.group,
  432. EventFixture({
  433. size: 1,
  434. dateCreated: '2019-03-20T00:00:00.000Z',
  435. errors: [],
  436. entries: [],
  437. tags: [{key: 'environment', value: 'dev'}],
  438. previousEventID: 'prev-event-id',
  439. nextEventID: 'next-event-id',
  440. })
  441. );
  442. MockApiClient.addMockResponse({
  443. url: `/projects/${props.organization.slug}/${props.project.slug}/events/${props.event.id}/committers/`,
  444. body: {
  445. committers: [
  446. {
  447. commits: [CommitFixture({author: CommitAuthorFixture()})],
  448. author: CommitAuthorFixture(),
  449. },
  450. ],
  451. },
  452. });
  453. render(<TestComponent project={props.project} />, {organization: props.organization});
  454. expect(await screen.findByTestId(/suspect-commit/)).toBeInTheDocument();
  455. });
  456. });
  457. describe('Platform Integrations', () => {
  458. let componentsRequest;
  459. beforeEach(() => {
  460. MockApiClient.clearMockResponses();
  461. });
  462. it('loads Integration UI components', async () => {
  463. const props = makeDefaultMockData();
  464. const unpublishedIntegration = SentryAppFixture({status: 'unpublished'});
  465. const internalIntegration = SentryAppFixture({status: 'internal'});
  466. const unpublishedInstall = SentryAppInstallationFixture({
  467. app: {
  468. slug: unpublishedIntegration.slug,
  469. uuid: unpublishedIntegration.uuid,
  470. },
  471. });
  472. const internalInstall = SentryAppInstallationFixture({
  473. app: {
  474. slug: internalIntegration.slug,
  475. uuid: internalIntegration.uuid,
  476. },
  477. });
  478. mockGroupApis(
  479. props.organization,
  480. props.project,
  481. props.group,
  482. EventFixture({
  483. size: 1,
  484. dateCreated: '2019-03-20T00:00:00.000Z',
  485. errors: [],
  486. entries: [],
  487. tags: [{key: 'environment', value: 'dev'}],
  488. previousEventID: 'prev-event-id',
  489. nextEventID: 'next-event-id',
  490. })
  491. );
  492. const component = SentryAppComponentFixture({
  493. sentryApp: {
  494. uuid: unpublishedIntegration.uuid,
  495. slug: unpublishedIntegration.slug,
  496. name: unpublishedIntegration.name,
  497. },
  498. });
  499. MockApiClient.addMockResponse({
  500. url: `/organizations/${props.organization.slug}/sentry-app-installations/`,
  501. body: [unpublishedInstall, internalInstall],
  502. });
  503. componentsRequest = MockApiClient.addMockResponse({
  504. url: `/organizations/${props.organization.slug}/sentry-app-components/`,
  505. body: [component],
  506. match: [MockApiClient.matchQuery({projectId: props.project.id})],
  507. });
  508. render(<TestComponent />, {organization: props.organization});
  509. expect(await screen.findByText('Sample App Issue')).toBeInTheDocument();
  510. expect(componentsRequest).toHaveBeenCalled();
  511. });
  512. describe('ANR Root Cause', () => {
  513. beforeEach(() => {
  514. MockApiClient.clearMockResponses();
  515. });
  516. it('shows anr root cause', async () => {
  517. const {organization} = initializeOrg();
  518. const props = makeDefaultMockData({
  519. ...organization,
  520. features: ['anr-improvements'],
  521. });
  522. mockGroupApis(
  523. props.organization,
  524. props.project,
  525. props.group,
  526. props.event,
  527. mockedTrace(props.project)
  528. );
  529. const routerContext = RouterContextFixture();
  530. render(<TestComponent group={props.group} event={props.event} />, {
  531. organization: props.organization,
  532. context: routerContext,
  533. });
  534. expect(
  535. await screen.findByRole('heading', {
  536. name: /suspect root cause/i,
  537. })
  538. ).toBeInTheDocument();
  539. expect(screen.getByText('File IO on Main Thread')).toBeInTheDocument();
  540. });
  541. it('does not render root issues section if related perf issues do not exist', async () => {
  542. const {organization} = initializeOrg();
  543. const props = makeDefaultMockData({
  544. ...organization,
  545. features: ['anr-improvements'],
  546. });
  547. const trace = mockedTrace(props.project);
  548. mockGroupApis(props.organization, props.project, props.group, props.event, {
  549. ...trace,
  550. performance_issues: [],
  551. });
  552. const routerContext = RouterContextFixture();
  553. render(<TestComponent group={props.group} event={props.event} />, {
  554. organization: props.organization,
  555. context: routerContext,
  556. });
  557. // mechanism: ANR
  558. expect(await screen.findByText('ANR')).toBeInTheDocument();
  559. expect(
  560. screen.queryByRole('heading', {
  561. name: /suspect root issues/i,
  562. })
  563. ).not.toBeInTheDocument();
  564. expect(screen.queryByText('File IO on Main Thread')).not.toBeInTheDocument();
  565. });
  566. });
  567. });