groupEventDetails.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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. };
  270. describe('groupEventDetails', () => {
  271. beforeEach(() => {
  272. MockApiClient.clearMockResponses();
  273. });
  274. afterEach(function () {
  275. MockApiClient.clearMockResponses();
  276. (browserHistory.replace as jest.Mock).mockClear();
  277. });
  278. it('redirects on switching to an invalid environment selection for event', async function () {
  279. const props = makeDefaultMockData();
  280. mockGroupApis(props.organization, props.project, props.group, props.event);
  281. const {rerender} = render(<TestComponent {...props} />, {
  282. organization: props.organization,
  283. });
  284. expect(browserHistory.replace).not.toHaveBeenCalled();
  285. rerender(<TestComponent environments={['prod']} />);
  286. await waitFor(() => expect(browserHistory.replace).toHaveBeenCalled());
  287. });
  288. it('does not redirect when switching to a valid environment selection for event', async function () {
  289. const props = makeDefaultMockData();
  290. mockGroupApis(props.organization, props.project, props.group, props.event);
  291. const {rerender} = render(<TestComponent {...props} />, {
  292. organization: props.organization,
  293. });
  294. expect(browserHistory.replace).not.toHaveBeenCalled();
  295. rerender(<TestComponent environments={[]} />);
  296. expect(await screen.findByTestId('group-event-details')).toBeInTheDocument();
  297. expect(browserHistory.replace).not.toHaveBeenCalled();
  298. });
  299. it('displays error on event error', async function () {
  300. const props = makeDefaultMockData();
  301. mockGroupApis(
  302. props.organization,
  303. props.project,
  304. props.group,
  305. EventFixture({
  306. size: 1,
  307. dateCreated: '2019-03-20T00:00:00.000Z',
  308. errors: [],
  309. entries: [],
  310. tags: [{key: 'environment', value: 'dev'}],
  311. previousEventID: 'prev-event-id',
  312. nextEventID: 'next-event-id',
  313. })
  314. );
  315. render(<TestComponent event={undefined} eventError />, {
  316. organization: props.organization,
  317. });
  318. expect(
  319. await screen.findByText(/events for this issue could not be found/)
  320. ).toBeInTheDocument();
  321. });
  322. it('renders the Span Evidence and Resources section for Performance Issues', async function () {
  323. const props = makeDefaultMockData();
  324. const group: Group = GroupFixture({
  325. issueCategory: IssueCategory.PERFORMANCE,
  326. issueType: IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES,
  327. });
  328. const transaction = EventFixture({
  329. entries: [{type: EntryType.SPANS, data: []}],
  330. });
  331. mockGroupApis(
  332. props.organization,
  333. props.project,
  334. props.group,
  335. EventFixture({
  336. size: 1,
  337. dateCreated: '2019-03-20T00:00:00.000Z',
  338. errors: [],
  339. entries: [],
  340. tags: [{key: 'environment', value: 'dev'}],
  341. previousEventID: 'prev-event-id',
  342. nextEventID: 'next-event-id',
  343. })
  344. );
  345. const routerContext = RouterContextFixture();
  346. render(<TestComponent group={group} event={transaction} />, {
  347. organization: props.organization,
  348. context: routerContext,
  349. });
  350. expect(
  351. await screen.findByRole('heading', {
  352. name: /span evidence/i,
  353. })
  354. ).toBeInTheDocument();
  355. expect(
  356. screen.getByRole('heading', {
  357. name: /resources/i,
  358. })
  359. ).toBeInTheDocument();
  360. });
  361. it('renders the Function Evidence and Resources section for Profile Issues', async function () {
  362. const props = makeDefaultMockData();
  363. const group: Group = GroupFixture({
  364. issueCategory: IssueCategory.PERFORMANCE,
  365. issueType: IssueType.PROFILE_FILE_IO_MAIN_THREAD,
  366. });
  367. const transaction = EventFixture({
  368. entries: [],
  369. occurrence: {
  370. evidenceDisplay: [],
  371. evidenceData: {
  372. templateName: 'profile',
  373. },
  374. type: 2001,
  375. },
  376. });
  377. mockGroupApis(
  378. props.organization,
  379. props.project,
  380. props.group,
  381. EventFixture({
  382. size: 1,
  383. dateCreated: '2019-03-20T00:00:00.000Z',
  384. errors: [],
  385. entries: [],
  386. tags: [{key: 'environment', value: 'dev'}],
  387. previousEventID: 'prev-event-id',
  388. nextEventID: 'next-event-id',
  389. })
  390. );
  391. const routerContext = RouterContextFixture();
  392. render(<TestComponent group={group} event={transaction} />, {
  393. organization: props.organization,
  394. context: routerContext,
  395. });
  396. expect(
  397. await screen.findByRole('heading', {
  398. name: /function evidence/i,
  399. })
  400. ).toBeInTheDocument();
  401. expect(
  402. screen.getByRole('heading', {
  403. name: /resources/i,
  404. })
  405. ).toBeInTheDocument();
  406. });
  407. });
  408. describe('EventCause', () => {
  409. beforeEach(() => {
  410. MockApiClient.clearMockResponses();
  411. });
  412. afterEach(function () {
  413. MockApiClient.clearMockResponses();
  414. (browserHistory.replace as jest.Mock).mockClear();
  415. });
  416. it('renders suspect commit', async function () {
  417. const props = makeDefaultMockData(
  418. undefined,
  419. ProjectFixture({firstEvent: EventFixture().dateCreated})
  420. );
  421. mockGroupApis(
  422. props.organization,
  423. props.project,
  424. props.group,
  425. EventFixture({
  426. size: 1,
  427. dateCreated: '2019-03-20T00:00:00.000Z',
  428. errors: [],
  429. entries: [],
  430. tags: [{key: 'environment', value: 'dev'}],
  431. previousEventID: 'prev-event-id',
  432. nextEventID: 'next-event-id',
  433. })
  434. );
  435. MockApiClient.addMockResponse({
  436. url: `/projects/${props.organization.slug}/${props.project.slug}/events/${props.event.id}/committers/`,
  437. body: {
  438. committers: [
  439. {
  440. commits: [CommitFixture({author: CommitAuthorFixture()})],
  441. author: CommitAuthorFixture(),
  442. },
  443. ],
  444. },
  445. });
  446. render(<TestComponent project={props.project} />, {organization: props.organization});
  447. expect(await screen.findByTestId(/suspect-commit/)).toBeInTheDocument();
  448. });
  449. });
  450. describe('Platform Integrations', () => {
  451. let componentsRequest;
  452. beforeEach(() => {
  453. MockApiClient.clearMockResponses();
  454. });
  455. it('loads Integration UI components', async () => {
  456. const props = makeDefaultMockData();
  457. const unpublishedIntegration = SentryAppFixture({status: 'unpublished'});
  458. const internalIntegration = SentryAppFixture({status: 'internal'});
  459. const unpublishedInstall = SentryAppInstallationFixture({
  460. app: {
  461. slug: unpublishedIntegration.slug,
  462. uuid: unpublishedIntegration.uuid,
  463. },
  464. });
  465. const internalInstall = SentryAppInstallationFixture({
  466. app: {
  467. slug: internalIntegration.slug,
  468. uuid: internalIntegration.uuid,
  469. },
  470. });
  471. mockGroupApis(
  472. props.organization,
  473. props.project,
  474. props.group,
  475. EventFixture({
  476. size: 1,
  477. dateCreated: '2019-03-20T00:00:00.000Z',
  478. errors: [],
  479. entries: [],
  480. tags: [{key: 'environment', value: 'dev'}],
  481. previousEventID: 'prev-event-id',
  482. nextEventID: 'next-event-id',
  483. })
  484. );
  485. const component = SentryAppComponentFixture({
  486. sentryApp: {
  487. uuid: unpublishedIntegration.uuid,
  488. slug: unpublishedIntegration.slug,
  489. name: unpublishedIntegration.name,
  490. },
  491. });
  492. MockApiClient.addMockResponse({
  493. url: `/organizations/${props.organization.slug}/sentry-app-installations/`,
  494. body: [unpublishedInstall, internalInstall],
  495. });
  496. componentsRequest = MockApiClient.addMockResponse({
  497. url: `/organizations/${props.organization.slug}/sentry-app-components/`,
  498. body: [component],
  499. match: [MockApiClient.matchQuery({projectId: props.project.id})],
  500. });
  501. render(<TestComponent />, {organization: props.organization});
  502. expect(await screen.findByText('Sample App Issue')).toBeInTheDocument();
  503. expect(componentsRequest).toHaveBeenCalled();
  504. });
  505. describe('ANR Root Cause', () => {
  506. beforeEach(() => {
  507. MockApiClient.clearMockResponses();
  508. });
  509. it('shows anr root cause', async () => {
  510. const {organization} = initializeOrg();
  511. const props = makeDefaultMockData({
  512. ...organization,
  513. features: ['anr-improvements'],
  514. });
  515. mockGroupApis(
  516. props.organization,
  517. props.project,
  518. props.group,
  519. props.event,
  520. mockedTrace(props.project)
  521. );
  522. const routerContext = RouterContextFixture();
  523. render(<TestComponent group={props.group} event={props.event} />, {
  524. organization: props.organization,
  525. context: routerContext,
  526. });
  527. expect(
  528. await screen.findByRole('heading', {
  529. name: /suspect root cause/i,
  530. })
  531. ).toBeInTheDocument();
  532. expect(screen.getByText('File IO on Main Thread')).toBeInTheDocument();
  533. });
  534. it('does not render root issues section if related perf issues do not exist', async () => {
  535. const {organization} = initializeOrg();
  536. const props = makeDefaultMockData({
  537. ...organization,
  538. features: ['anr-improvements'],
  539. });
  540. const trace = mockedTrace(props.project);
  541. mockGroupApis(props.organization, props.project, props.group, props.event, {
  542. ...trace,
  543. performance_issues: [],
  544. });
  545. const routerContext = RouterContextFixture();
  546. render(<TestComponent group={props.group} event={props.event} />, {
  547. organization: props.organization,
  548. context: routerContext,
  549. });
  550. // mechanism: ANR
  551. expect(await screen.findByText('ANR')).toBeInTheDocument();
  552. expect(
  553. screen.queryByRole('heading', {
  554. name: /suspect root issues/i,
  555. })
  556. ).not.toBeInTheDocument();
  557. expect(screen.queryByText('File IO on Main Thread')).not.toBeInTheDocument();
  558. });
  559. });
  560. });