stacktraceLink.spec.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import {CommitFixture} from 'sentry-fixture/commit';
  2. import {EventFixture} from 'sentry-fixture/event';
  3. import {GitHubIntegrationFixture} from 'sentry-fixture/githubIntegration';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {ProjectFixture} from 'sentry-fixture/project';
  6. import {ReleaseFixture} from 'sentry-fixture/release';
  7. import {RepositoryFixture} from 'sentry-fixture/repository';
  8. import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig';
  9. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  10. import HookStore from 'sentry/stores/hookStore';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import type {Frame} from 'sentry/types';
  13. import {CodecovStatusCode} from 'sentry/types';
  14. import * as analytics from 'sentry/utils/analytics';
  15. import {StacktraceLink} from './stacktraceLink';
  16. describe('StacktraceLink', function () {
  17. const org = OrganizationFixture();
  18. const platform = 'python';
  19. const project = ProjectFixture({});
  20. const event = EventFixture({
  21. projectID: project.id,
  22. release: ReleaseFixture({lastCommit: CommitFixture()}),
  23. platform,
  24. });
  25. const integration = GitHubIntegrationFixture();
  26. const repo = RepositoryFixture({integrationId: integration.id});
  27. const frame = {filename: '/sentry/app.py', lineNo: 233, inApp: true} as Frame;
  28. const config = RepositoryProjectPathConfigFixture({project, repo, integration});
  29. const analyticsSpy = jest.spyOn(analytics, 'trackAnalytics');
  30. beforeEach(function () {
  31. jest.clearAllMocks();
  32. MockApiClient.clearMockResponses();
  33. ProjectsStore.loadInitialData([project]);
  34. HookStore.init?.();
  35. });
  36. it('renders nothing when missing integrations', async function () {
  37. MockApiClient.addMockResponse({
  38. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  39. body: {config: null, sourceUrl: null, integrations: []},
  40. });
  41. const {container} = render(<StacktraceLink frame={frame} event={event} line="" />);
  42. await waitFor(() => {
  43. expect(container).toBeEmptyDOMElement();
  44. });
  45. });
  46. it('renders setup CTA with integration but no configs', async function () {
  47. MockApiClient.addMockResponse({
  48. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  49. body: {config: null, sourceUrl: null, integrations: [integration]},
  50. });
  51. render(<StacktraceLink frame={frame} event={event} line="foo()" />);
  52. expect(await screen.findByText('Set up Code Mapping')).toBeInTheDocument();
  53. });
  54. it('renders source url link', async function () {
  55. MockApiClient.addMockResponse({
  56. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  57. body: {config, sourceUrl: 'https://something.io', integrations: [integration]},
  58. });
  59. render(<StacktraceLink frame={frame} event={event} line="foo()" />);
  60. const link = await screen.findByRole('link', {name: 'Open this line in GitHub'});
  61. expect(link).toBeInTheDocument();
  62. expect(link).toHaveAttribute('href', 'https://something.io#L233');
  63. });
  64. it('displays fix modal on error', async function () {
  65. MockApiClient.addMockResponse({
  66. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  67. body: {
  68. config,
  69. sourceUrl: null,
  70. integrations: [integration],
  71. },
  72. });
  73. render(<StacktraceLink frame={frame} event={event} line="foo()" />);
  74. expect(
  75. await screen.findByRole('button', {name: 'Set up Code Mapping'})
  76. ).toBeInTheDocument();
  77. });
  78. it('should hide stacktrace link error state on minified javascript frames', async function () {
  79. MockApiClient.addMockResponse({
  80. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  81. body: {
  82. config,
  83. sourceUrl: null,
  84. integrations: [integration],
  85. },
  86. });
  87. const {container} = render(
  88. <StacktraceLink
  89. frame={frame}
  90. event={{...event, platform: 'javascript'}}
  91. line="{snip} somethingInsane=e.IsNotFound {snip}"
  92. />
  93. );
  94. await waitFor(() => {
  95. expect(container).toBeEmptyDOMElement();
  96. });
  97. });
  98. it('should hide stacktrace link error state on unsupported platforms', async function () {
  99. MockApiClient.addMockResponse({
  100. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  101. body: {
  102. config,
  103. sourceUrl: null,
  104. integrations: [integration],
  105. },
  106. });
  107. const {container} = render(
  108. <StacktraceLink frame={frame} event={{...event, platform: 'unreal'}} line="" />
  109. );
  110. await waitFor(() => {
  111. expect(container).toBeEmptyDOMElement();
  112. });
  113. });
  114. it('renders the codecov link', async function () {
  115. const organization = {
  116. ...org,
  117. codecovAccess: true,
  118. features: ['codecov-integration'],
  119. };
  120. MockApiClient.addMockResponse({
  121. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  122. body: {
  123. config,
  124. sourceUrl: 'https://github.com/username/path/to/file.py',
  125. integrations: [integration],
  126. },
  127. });
  128. MockApiClient.addMockResponse({
  129. url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
  130. body: {
  131. status: CodecovStatusCode.COVERAGE_EXISTS,
  132. lineCoverage: [[233, 0]],
  133. coverageUrl: 'https://app.codecov.io/gh/path/to/file.py',
  134. },
  135. });
  136. render(<StacktraceLink frame={frame} event={event} line="foo()" />, {
  137. organization,
  138. });
  139. const link = await screen.findByRole('link', {name: 'Open in Codecov'});
  140. expect(link).toBeInTheDocument();
  141. expect(link).toHaveAttribute(
  142. 'href',
  143. 'https://app.codecov.io/gh/path/to/file.py#L233'
  144. );
  145. await userEvent.click(link);
  146. expect(analyticsSpy).toHaveBeenCalledWith(
  147. 'integrations.stacktrace_codecov_link_clicked',
  148. expect.anything()
  149. );
  150. });
  151. it('renders the missing coverage warning', async function () {
  152. const organization = {
  153. ...org,
  154. codecovAccess: true,
  155. features: ['codecov-integration'],
  156. };
  157. MockApiClient.addMockResponse({
  158. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  159. body: {
  160. config,
  161. sourceUrl: 'https://github.com/username/path/to/file.py',
  162. integrations: [integration],
  163. },
  164. });
  165. MockApiClient.addMockResponse({
  166. url: `/projects/${org.slug}/${project.slug}/stacktrace-coverage/`,
  167. body: {status: CodecovStatusCode.NO_COVERAGE_DATA},
  168. });
  169. render(<StacktraceLink frame={frame} event={event} line="foo()" />, {
  170. organization,
  171. });
  172. expect(await screen.findByText('Code Coverage not found')).toBeInTheDocument();
  173. });
  174. it('renders the link using a valid sourceLink for a .NET project', async function () {
  175. const dotnetFrame = {
  176. filename: 'path/to/file.py',
  177. sourceLink: 'https://www.github.com/username/path/to/file.py#L100',
  178. lineNo: '100',
  179. } as unknown as Frame;
  180. MockApiClient.addMockResponse({
  181. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  182. body: {
  183. config,
  184. integrations: [integration],
  185. },
  186. });
  187. render(
  188. <StacktraceLink
  189. frame={dotnetFrame}
  190. event={{...event, platform: 'csharp'}}
  191. line="foo()"
  192. />
  193. );
  194. const link = await screen.findByRole('link', {name: 'GitHub'});
  195. expect(link).toBeInTheDocument();
  196. expect(link).toHaveAttribute(
  197. 'href',
  198. 'https://www.github.com/username/path/to/file.py#L100'
  199. );
  200. });
  201. it('hides stacktrace link if there is no source link for .NET projects', async function () {
  202. MockApiClient.addMockResponse({
  203. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  204. body: {
  205. config,
  206. integrations: [integration],
  207. },
  208. });
  209. const {container} = render(
  210. <StacktraceLink frame={frame} event={{...event, platform: 'csharp'}} line="" />
  211. );
  212. await waitFor(() => {
  213. expect(container).toBeEmptyDOMElement();
  214. });
  215. });
  216. it('renders in-frame stacktrace links and fetches data with 100ms delay', async function () {
  217. const mockRequest = MockApiClient.addMockResponse({
  218. url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
  219. body: {config, sourceUrl: 'https://something.io', integrations: [integration]},
  220. });
  221. render(<StacktraceLink frame={frame} event={event} line="foo()" />);
  222. const link = await screen.findByRole('link', {name: 'Open this line in GitHub'});
  223. expect(link).toBeInTheDocument();
  224. expect(link).toHaveAttribute('href', 'https://something.io#L233');
  225. // The link is an icon with aira label
  226. expect(link).toHaveTextContent('');
  227. expect(mockRequest).toHaveBeenCalledTimes(1);
  228. });
  229. });