123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- import {useEffect, useMemo, useState} from 'react';
- import {css, keyframes} from '@emotion/react';
- import styled from '@emotion/styled';
- import {openModal} from 'sentry/actionCreators/modal';
- import {Button} from 'sentry/components/button';
- import {useStacktraceCoverage} from 'sentry/components/events/interfaces/frame/useStacktraceCoverage';
- import {hasFileExtension} from 'sentry/components/events/interfaces/frame/utils';
- import ExternalLink from 'sentry/components/links/externalLink';
- import Placeholder from 'sentry/components/placeholder';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconWarning} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {
- Event,
- Frame,
- Organization,
- PlatformKey,
- StacktraceLinkResult,
- } from 'sentry/types';
- import {CodecovStatusCode} from 'sentry/types';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {getAnalyticsDataForEvent} from 'sentry/utils/events';
- import {getIntegrationIcon, getIntegrationSourceUrl} from 'sentry/utils/integrationUtil';
- import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import StacktraceLinkModal from './stacktraceLinkModal';
- import useStacktraceLink from './useStacktraceLink';
- const supportedStacktracePlatforms: PlatformKey[] = [
- 'go',
- 'javascript',
- 'node',
- 'php',
- 'python',
- 'ruby',
- 'elixir',
- ];
- function shouldShowCodecovFeatures(
- organization: Organization,
- match: StacktraceLinkResult,
- codecovStatus: CodecovStatusCode
- ) {
- const validStatus = codecovStatus && codecovStatus !== CodecovStatusCode.NO_INTEGRATION;
- return (
- organization.codecovAccess && validStatus && match.config?.provider.key === 'github'
- );
- }
- interface CodecovLinkProps {
- event: Event;
- organization: Organization;
- coverageUrl?: string;
- status?: CodecovStatusCode;
- }
- function CodecovLink({
- coverageUrl,
- status = CodecovStatusCode.COVERAGE_EXISTS,
- organization,
- event,
- }: CodecovLinkProps) {
- if (status === CodecovStatusCode.NO_COVERAGE_DATA) {
- return (
- <CodecovWarning>
- {t('Code Coverage not found')}
- <IconWarning size="xs" color="errorText" />
- </CodecovWarning>
- );
- }
- if (status !== CodecovStatusCode.COVERAGE_EXISTS || !coverageUrl) {
- return null;
- }
- const onOpenCodecovLink = (e: React.MouseEvent) => {
- e.stopPropagation();
- trackAnalytics('integrations.stacktrace_codecov_link_clicked', {
- view: 'stacktrace_issue_details',
- organization,
- group_id: event.groupID ? parseInt(event.groupID, 10) : -1,
- ...getAnalyticsDataForEvent(event),
- });
- };
- return (
- <OpenInLink
- href={coverageUrl}
- openInNewTab
- onClick={onOpenCodecovLink}
- aria-label={t('Open in Codecov')}
- >
- <Tooltip title={t('Open in Codecov')} skipWrapper>
- <StyledIconWrapper>{getIntegrationIcon('codecov', 'sm')}</StyledIconWrapper>
- </Tooltip>
- </OpenInLink>
- );
- }
- interface StacktraceLinkProps {
- event: Event;
- frame: Frame;
- /**
- * The line of code being linked
- */
- line: string;
- }
- export function StacktraceLink({frame, event, line}: StacktraceLinkProps) {
- const organization = useOrganization();
- const {projects} = useProjects();
- const validFilePath = hasFileExtension(frame.absPath || frame.filename || '');
- // TODO: Currently we only support GitHub links. Implement support for other source code providers.
- // Related comment: https://github.com/getsentry/sentry/pull/62596#discussion_r1443025242
- const hasGithubSourceLink = (frame.sourceLink || '').startsWith(
- 'https://www.github.com/'
- );
- const [isQueryEnabled, setIsQueryEnabled] = useState(
- hasGithubSourceLink ? false : !frame.inApp
- );
- const project = useMemo(
- () => projects.find(p => p.id === event.projectID),
- [projects, event]
- );
- useEffect(() => {
- let timer: ReturnType<typeof setTimeout> | undefined;
- if (!validFilePath) {
- return setIsQueryEnabled(false);
- }
- // Skip fetching if we already have the Source Link
- if (!hasGithubSourceLink && frame.inApp) {
- // Introduce a delay before enabling the query
- timer = setTimeout(() => {
- setIsQueryEnabled(true);
- }, 100); // Delay of 100ms
- }
- return () => timer && clearTimeout(timer);
- }, [validFilePath, hasGithubSourceLink, frame]);
- const {
- data: match,
- isLoading,
- refetch,
- } = useStacktraceLink(
- {
- event,
- frame,
- orgSlug: organization.slug,
- projectSlug: project?.slug,
- },
- {
- enabled: isQueryEnabled, // The query will not run until `isQueryEnabled` is true
- }
- );
- const coverageEnabled =
- isQueryEnabled && organization.features.includes('codecov-integration');
- const {data: coverage, isLoading: isLoadingCoverage} = useStacktraceCoverage(
- {
- event,
- frame,
- orgSlug: organization.slug,
- projectSlug: project?.slug,
- },
- {
- enabled: coverageEnabled,
- }
- );
- useRouteAnalyticsParams(
- match
- ? {
- stacktrace_link_viewed: true,
- stacktrace_link_status: match.sourceUrl
- ? 'match'
- : match.error || match.integrations.length
- ? 'no_match'
- : 'empty',
- }
- : {}
- );
- const onOpenLink = (e: React.MouseEvent, sourceLink: Frame['sourceLink'] = null) => {
- e.stopPropagation();
- const provider = match?.config?.provider;
- if (provider) {
- trackAnalytics(
- 'integrations.stacktrace_link_clicked',
- {
- view: 'stacktrace_issue_details',
- provider: provider.key,
- organization,
- group_id: event.groupID ? parseInt(event.groupID, 10) : -1,
- ...getAnalyticsDataForEvent(event),
- },
- {startSession: true}
- );
- }
- if (sourceLink) {
- const url = new URL(sourceLink);
- const hostname = url.hostname;
- const parts = hostname.split('.');
- const domain = parts.length > 1 ? parts[1] : '';
- trackAnalytics(
- 'integrations.non_inapp_stacktrace_link_clicked',
- {
- view: 'stacktrace_issue_details',
- provider: domain,
- organization,
- group_id: event.groupID ? parseInt(event.groupID, 10) : -1,
- ...getAnalyticsDataForEvent(event),
- },
- {startSession: true}
- );
- }
- };
- const handleSubmit = () => {
- refetch();
- };
- if (!validFilePath) {
- return null;
- }
- // Render the provided `sourceLink` for all the non-inapp frames for `csharp` platform Issues
- // We skip fetching from the API for these frames.
- if (!match && hasGithubSourceLink && !frame.inApp && frame.sourceLink) {
- return (
- <StacktraceLinkWrapper>
- <Tooltip title={t('Open this line in GitHub')} skipWrapper>
- <OpenInLink
- onClick={e => onOpenLink(e, frame.sourceLink)}
- href={frame.sourceLink}
- openInNewTab
- aria-label={t('GitHub')}
- >
- <StyledIconWrapper>{getIntegrationIcon('github', 'sm')}</StyledIconWrapper>
- </OpenInLink>
- </Tooltip>
- </StacktraceLinkWrapper>
- );
- }
- if (isLoading || !match) {
- return (
- <StacktraceLinkWrapper>
- <Placeholder height="14px" width="40px" />
- </StacktraceLinkWrapper>
- );
- }
- // Match found - display link to source
- if (match.config && match.sourceUrl) {
- const label = t('Open this line in %s', match.config.provider.name);
- return (
- <StacktraceLinkWrapper>
- <OpenInLink
- onClick={onOpenLink}
- href={getIntegrationSourceUrl(
- match.config.provider.key,
- match!.sourceUrl,
- frame.lineNo
- )}
- openInNewTab
- aria-label={label}
- >
- <Tooltip title={label} skipWrapper>
- <StyledIconWrapper>
- {getIntegrationIcon(match.config.provider.key, 'sm')}
- </StyledIconWrapper>
- </Tooltip>
- </OpenInLink>
- {coverageEnabled && isLoadingCoverage ? (
- <Placeholder height="14px" width="14px" />
- ) : coverage &&
- shouldShowCodecovFeatures(organization, match, coverage.status) ? (
- <CodecovLink
- coverageUrl={`${coverage.coverageUrl}#L${frame.lineNo}`}
- status={coverage.status}
- organization={organization}
- event={event}
- />
- ) : null}
- </StacktraceLinkWrapper>
- );
- }
- // Hide stacktrace link errors if the stacktrace might be minified javascript
- // Check if the line starts and ends with {snip}
- const isMinifiedJsError =
- event.platform === 'javascript' && /(\{snip\}).*\1/.test(line);
- const isUnsupportedPlatform = !supportedStacktracePlatforms.includes(
- event.platform as PlatformKey
- );
- 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>
- <Tooltip title={t('GitHub')} skipWrapper>
- <OpenInLink onClick={onOpenLink} href={frame.sourceLink} openInNewTab>
- <StyledIconWrapper>{getIntegrationIcon('github', 'sm')}</StyledIconWrapper>
- </OpenInLink>
- </Tooltip>
- {coverageEnabled && isLoadingCoverage ? (
- <Placeholder height="14px" width="14px" />
- ) : coverage &&
- shouldShowCodecovFeatures(organization, match, coverage.status) ? (
- <CodecovLink
- coverageUrl={`${frame.sourceLink}`}
- status={coverage.status}
- organization={organization}
- event={event}
- />
- ) : null}
- </StacktraceLinkWrapper>
- );
- }
- // No match found - Has integration but no code mappings
- if (!hideErrors && (match.error || match.integrations.length > 0)) {
- const filename = frame.filename;
- if (!project || !match.integrations.length || !filename) {
- return null;
- }
- const sourceCodeProviders = match.integrations.filter(integration =>
- ['github', 'gitlab'].includes(integration.provider?.key)
- );
- return (
- <StacktraceLinkWrapper>
- <FixMappingButton
- priority="link"
- icon={
- sourceCodeProviders.length === 1
- ? getIntegrationIcon(sourceCodeProviders[0].provider.key, 'sm')
- : undefined
- }
- onClick={() => {
- trackAnalytics(
- 'integrations.stacktrace_start_setup',
- {
- view: 'stacktrace_issue_details',
- platform: event.platform,
- provider: sourceCodeProviders[0]?.provider.key,
- setup_type: 'automatic',
- organization,
- ...getAnalyticsDataForEvent(event),
- },
- {startSession: true}
- );
- openModal(deps => (
- <StacktraceLinkModal
- onSubmit={handleSubmit}
- filename={filename}
- project={project}
- organization={organization}
- integrations={match.integrations}
- {...deps}
- />
- ));
- }}
- >
- {t('Set up Code Mapping')}
- </FixMappingButton>
- </StacktraceLinkWrapper>
- );
- }
- return null;
- }
- const fadeIn = keyframes`
- from { opacity: 0; }
- to { opacity: 1; }
- `;
- const StacktraceLinkWrapper = styled('div')`
- display: flex;
- gap: ${space(1)};
- align-items: center;
- color: ${p => p.theme.subText};
- font-family: ${p => p.theme.text.family};
- padding: ${space(0)} ${space(1)};
- `;
- const FixMappingButton = styled(Button)`
- color: ${p => p.theme.subText};
- &:hover {
- color: ${p => p.theme.subText};
- }
- `;
- const StyledIconWrapper = styled('span')`
- color: inherit;
- line-height: 0;
- `;
- const LinkStyles = css`
- display: flex;
- align-items: center;
- gap: ${space(0.75)};
- `;
- const OpenInLink = styled(ExternalLink)`
- ${LinkStyles}
- color: ${p => p.theme.subText};
- animation: ${fadeIn} 0.2s ease-in-out forwards;
- &:hover {
- color: ${p => p.theme.textColor};
- }
- `;
- const CodecovWarning = styled('div')`
- display: flex;
- color: ${p => p.theme.errorText};
- gap: ${space(0.75)};
- align-items: center;
- `;
|