import {Fragment} from 'react';
import styled from '@emotion/styled';
import ExternalLink from 'sentry/components/links/externalLink';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event, Frame} from 'sentry/types/event';
import type {TagWithTopValues} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
import type {Project} from 'sentry/types/project';
import {uniq} from 'sentry/utils/array/uniq';
import {useApiQuery} from 'sentry/utils/queryClient';
import {safeURL} from 'sentry/utils/url/safeURL';
import {useUser} from 'sentry/utils/useUser';
import OwnerInput from 'sentry/views/settings/project/projectOwnership/ownerInput';
type IssueOwnershipResponse = {
autoAssignment: boolean;
dateCreated: string;
fallthrough: boolean;
isActive: boolean;
lastUpdated: string;
raw: string;
};
type Props = {
issueId: string;
onCancel: () => void;
organization: Organization;
project: Project;
eventData?: Event;
};
function getFrameSuggestions(eventData?: Event) {
// pull frame data out of exception or the stacktrace
const entry = eventData?.entries?.find(({type}) =>
['exception', 'stacktrace'].includes(type)
);
let frames: Frame[] = [];
if (entry?.type === 'exception') {
frames = entry?.data?.values?.[0]?.stacktrace?.frames ?? [];
} else if (entry?.type === 'stacktrace') {
frames = entry?.data?.frames ?? [];
}
// Only display in-app frames
frames = frames.filter(frame => frame?.inApp).reverse();
return uniq(frames.map(frame => frame.filename || frame.absPath || ''));
}
/**
* Attempt to remove the origin from a URL
*/
function getUrlPath(maybeUrl?: string) {
if (!maybeUrl) {
return '';
}
const parsedURL = safeURL(maybeUrl);
if (!parsedURL) {
return maybeUrl;
}
return `*${parsedURL.pathname}`;
}
function OwnershipSuggestions({
paths,
urls,
eventData,
}: {
paths: string[];
urls: string[];
eventData?: Event;
}) {
const user = useUser();
if (!user.email) {
return null;
}
const pathSuggestion = paths.length ? `path:${paths[0]} ${user.email}` : null;
const urlSuggestion = urls.length ? `url:${getUrlPath(urls[0])} ${user.email}` : null;
const transactionTag = eventData?.tags?.find(({key}) => key === 'transaction');
const transactionSuggestion = transactionTag
? `tags.transaction:${transactionTag.value} ${user.email}`
: null;
return (
# {t('Here’s some suggestions based on this issue')}
{[pathSuggestion, urlSuggestion, transactionSuggestion]
.filter(x => x)
.map(suggestion => (
{suggestion}
))}
);
}
function ProjectOwnershipModal({
organization,
project,
issueId,
eventData,
onCancel,
}: Props) {
const {
data: urlTagData,
isPending: isUrlTagDataPending,
isError: isUrlTagDataError,
error,
} = useApiQuery([`/issues/${issueId}/tags/url/`], {staleTime: 0});
const {
data: ownership,
isPending: isOwnershipPending,
isError: isOwnershipError,
} = useApiQuery(
[`/projects/${organization.slug}/${project.slug}/ownership/`],
{
staleTime: 0,
}
);
if (isOwnershipPending || isUrlTagDataPending) {
return ;
}
if (isOwnershipError || (isUrlTagDataError && error.status !== 404)) {
return ;
}
if (!ownership) {
return null;
}
const urls = urlTagData
? urlTagData.topValues
.sort((a, b) => a.count - b.count)
.map(i => i.value)
.slice(0, 5)
: [];
const paths = getFrameSuggestions(eventData);
return (
{tct(
'Assign issues based on custom rules. To learn more, [docs:read the docs].',
{
docs: (
),
}
)}
);
}
const Description = styled('p')`
margin-bottom: ${space(1)};
`;
const StyledPre = styled('pre')`
word-break: break-word;
padding: ${space(2)};
line-height: 1.6;
color: ${p => p.theme.subText};
`;
export default ProjectOwnershipModal;