Browse Source

feat(stacktrace): Use source link as a stack trace link alternative for .NET projects (#57520)

This PR adds the first version of stack trace linking for .NET projects
to the UI as part of #53249.


![image](https://github.com/getsentry/sentry/assets/45607721/6673c136-d72c-4b00-a7df-b832729c9541)

### Logic
Context: On the backend, source links pointing to the
raw.githubusercontent domain are modified to become github.com URLs.
1. If the customer has a source code management tool installed for their
org, use it to see if a stack trace link can be found.
2. If no link can be found for ANY reason, the source link (retrieved
via frame) is used instead.
3. However, if source link is not a GitHub URL, do not display anything.
5. No prompt to add code mappings should be shown in any case.
Isabella Enriquez 1 year ago
parent
commit
7090405ecc

+ 76 - 0
static/app/components/events/interfaces/frame/stacktraceLink.spec.tsx

@@ -288,4 +288,80 @@ describe('StacktraceLink', function () {
     });
     expect(await screen.findByTestId('codecov-link')).toBeInTheDocument();
   });
+
+  it('renders the link using a valid sourceLink for a .NET project', async function () {
+    const dotnetFrame = {
+      sourceLink: 'https://www.github.com/username/path/to/file.py#L100',
+      lineNo: '100',
+    } as unknown as Frame;
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
+      body: {
+        config,
+        integrations: [integration],
+      },
+    });
+    render(
+      <StacktraceLink
+        frame={dotnetFrame}
+        event={{...event, platform: 'csharp'}}
+        line="foo()"
+      />,
+      {
+        context: TestStubs.routerContext(),
+      }
+    );
+    expect(await screen.findByRole('link')).toHaveAttribute(
+      'href',
+      'https://www.github.com/username/path/to/file.py#L100'
+    );
+    expect(screen.getByText('Open this line in GitHub')).toBeInTheDocument();
+  });
+
+  it('renders the link using sourceUrl instead of sourceLink if it exists for a .NET project', async function () {
+    const dotnetFrame = {
+      sourceLink: 'https://www.github.com/source/link/url#L1',
+      lineNo: '1',
+    } as unknown as Frame;
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
+      body: {
+        config,
+        sourceUrl: 'https://www.github.com/url/from/code/mapping',
+        integrations: [integration],
+      },
+    });
+    render(
+      <StacktraceLink
+        frame={dotnetFrame}
+        event={{...event, platform: 'csharp'}}
+        line="foo()"
+      />,
+      {
+        context: TestStubs.routerContext(),
+      }
+    );
+    expect(await screen.findByRole('link')).toHaveAttribute(
+      'href',
+      'https://www.github.com/url/from/code/mapping#L1'
+    );
+    expect(screen.getByText('Open this line in GitHub')).toBeInTheDocument();
+  });
+
+  it('hides stacktrace link if there is no source link for .NET projects', async function () {
+    MockApiClient.addMockResponse({
+      url: `/projects/${org.slug}/${project.slug}/stacktrace-link/`,
+      body: {
+        config,
+        integrations: [integration],
+      },
+    });
+    const {container} = render(
+      <StacktraceLink frame={frame} event={{...event, platform: 'csharp'}} line="" />,
+      {context: TestStubs.routerContext()}
+    );
+    await waitFor(() => {
+      expect(container).toBeEmptyDOMElement();
+    });
+  });
 });

+ 30 - 0
static/app/components/events/interfaces/frame/stacktraceLink.tsx

@@ -303,7 +303,37 @@ export function StacktraceLink({frame, event, line}: StacktraceLinkProps) {
   const isUnsupportedPlatform = !supportedStacktracePlatforms.includes(
     event.platform as PlatformKey
   );
+  const hasGithubSourceLink =
+    event.platform === 'csharp' &&
+    frame.sourceLink?.startsWith('https://www.github.com/');
   const hideErrors = isMinifiedJsError || isUnsupportedPlatform;
+
+  // for .NET projects, if there is no match found but there is a GitHub source link, use that
+  if (
+    frame.sourceLink &&
+    hasGithubSourceLink &&
+    (match.error || match.integrations.length > 0)
+  ) {
+    return (
+      <StacktraceLinkWrapper>
+        <OpenInLink onClick={onOpenLink} href={frame.sourceLink} openInNewTab>
+          <StyledIconWrapper>{getIntegrationIcon('github', 'sm')}</StyledIconWrapper>
+          {t('Open this line in GitHub')}
+        </OpenInLink>
+        {shouldShowCodecovFeatures(organization, match) ? (
+          <CodecovLink
+            coverageUrl={`${frame.sourceLink}`}
+            status={match.codecov?.status}
+            organization={organization}
+            event={event}
+          />
+        ) : shouldShowCodecovPrompt(organization, match) ? (
+          <HookCodecovStacktraceLink organization={organization} />
+        ) : null}
+      </StacktraceLinkWrapper>
+    );
+  }
+
   // No match found - Has integration but no code mappings
   if (!hideErrors && (match.error || match.integrations.length > 0)) {
     const filename = frame.filename;

+ 1 - 0
static/app/types/event.tsx

@@ -195,6 +195,7 @@ export type Frame = {
   mapUrl?: string | null;
   minGroupingLevel?: number;
   origAbsPath?: string | null;
+  sourceLink?: string | null;
   symbolicatorStatus?: SymbolicatorStatus;
 };