import {Fragment, useCallback, useEffect, useState} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import type {Location} from 'history'; import * as qs from 'query-string'; import Alert from 'sentry/components/alert'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import HookOrDefault from 'sentry/components/hookOrDefault'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import {t} from 'sentry/locale'; import type {SimilarItem} from 'sentry/stores/groupingStore'; import GroupingStore from 'sentry/stores/groupingStore'; import {space} from 'sentry/styles/space'; import type {Project} from 'sentry/types/project'; import {useNavigate} from 'sentry/utils/useNavigate'; import usePrevious from 'sentry/utils/usePrevious'; import List from './list'; type RouteParams = { groupId: string; orgId: string; }; type Props = RouteComponentProps & { location: Location; project: Project; }; type ItemState = { filtered: SimilarItem[]; pageLinks: string | null; similar: SimilarItem[]; }; const DataConsentBanner = HookOrDefault({ hookName: 'component:data-consent-banner', defaultComponent: null, }); function SimilarStackTrace({params, location, project}: Props) { const {orgId, groupId} = params; const [items, setItems] = useState({ similar: [], filtered: [], pageLinks: null, }); const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading'); const navigate = useNavigate(); const prevLocationSearch = usePrevious(location.search); const hasSimilarityFeature = project.features.includes('similarity-view'); const hasSimilarityEmbeddingsFeature = project.features.includes('similarity-embeddings') || location.query.similarityEmbeddings === '1'; // Use reranking by default (assuming the `seer.similarity.similar_issues.use_reranking` // backend option is using its default value of `True`). This is just so we can turn it off // on demand to see if/how that changes the results. const useReranking = String(location.query.useReranking !== '0'); const fetchData = useCallback(() => { setStatus('loading'); const reqs: Parameters[0] = []; if (hasSimilarityEmbeddingsFeature) { reqs.push({ endpoint: `/organizations/${orgId}/issues/${groupId}/similar-issues-embeddings/?${qs.stringify( { k: 10, threshold: 0.01, useReranking, } )}`, dataKey: 'similar', }); } else if (hasSimilarityFeature) { reqs.push({ endpoint: `/organizations/${orgId}/issues/${groupId}/similar/?${qs.stringify({ ...location.query, limit: 50, })}`, dataKey: 'similar', }); } GroupingStore.onFetch(reqs); }, [ location.query, groupId, orgId, hasSimilarityFeature, hasSimilarityEmbeddingsFeature, useReranking, ]); const onGroupingChange = useCallback( ({ mergedParent, similarItems: updatedSimilarItems, filteredSimilarItems: updatedFilteredSimilarItems, similarLinks: updatedSimilarLinks, loading, error, }) => { if (updatedSimilarItems) { setItems({ similar: updatedSimilarItems, filtered: updatedFilteredSimilarItems, pageLinks: updatedSimilarLinks, }); setStatus(error ? 'error' : loading ? 'loading' : 'ready'); return; } if (mergedParent && mergedParent !== groupId) { // Merge success, since we can't specify target, we need to redirect to new parent navigate(`/organizations/${orgId}/issues/${mergedParent}/similar/`); } }, [navigate, groupId, orgId] ); useEffect(() => { fetchData(); }, [fetchData]); useEffect(() => { if (prevLocationSearch !== location.search) { fetchData(); } }, [fetchData, prevLocationSearch, location.search]); useEffect(() => { const unsubscribe = GroupingStore.listen(onGroupingChange, undefined); return () => { unsubscribe(); }; }, [onGroupingChange]); const handleMerge = useCallback(() => { if (!params) { return; } // You need at least 1 similarItem OR filteredSimilarItems to be able to merge, // so `firstIssue` should always exist from one of those lists. // // Similar issues API currently does not return issues across projects, // so we can assume that the first issues project slug is the project in // scope const [firstIssue] = items.similar.length ? items.similar : items.filtered; GroupingStore.onMerge({ params, query: location.query, projectId: firstIssue.issue.project.slug, }); }, [params, location.query, items]); const hasSimilarItems = (hasSimilarityFeature || hasSimilarityEmbeddingsFeature) && (items.similar.length > 0 || items.filtered.length > 0); return ( {hasSimilarityEmbeddingsFeature && ( Hi there! We're working on improving grouping with ML techniques. Each similar issue has been scored as "Would Group: Yes" and "Would Group: No," which refers to whether or not we'd group the similar issue into the main issue. )} {t('Issues with a similar stack trace')} {t( 'This is an experimental feature. Data may not be immediately available while we process merges.' )} {status === 'loading' && } {status === 'error' && ( )} {status === 'ready' && !hasSimilarItems && !hasSimilarityEmbeddingsFeature && (

{t("There don't seem to be any similar issues.")}

)} {status === 'ready' && !hasSimilarItems && hasSimilarityEmbeddingsFeature && (

{t( "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames." )}

)} {status === 'ready' && hasSimilarItems && !hasSimilarityEmbeddingsFeature && ( )} {status === 'ready' && hasSimilarItems && hasSimilarityEmbeddingsFeature && ( )}
); } export default SimilarStackTrace; const Title = styled('h4')` margin-bottom: ${space(0.75)}; `; const HeaderWrapper = styled('div')` margin-bottom: ${space(2)}; small { color: ${p => p.theme.subText}; } `;