import {Fragment, useCallback, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import * as qs from 'query-string'; 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 {useDetailedProject} from 'sentry/utils/useDetailedProject'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import usePrevious from 'sentry/utils/usePrevious'; import List from './list'; type Props = { project: Project; }; type ItemState = { filtered: SimilarItem[]; pageLinks: string | null; similar: SimilarItem[]; }; const DataConsentBanner = HookOrDefault({ hookName: 'component:data-consent-banner', defaultComponent: null, }); function SimilarStackTrace({project}: Props) { const location = useLocation(); const organization = useOrganization(); const params = useParams<{groupId: string; orgId: string}>(); 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 {data: projectData, isPending} = useDetailedProject({ orgSlug: organization.slug, projectSlug: project.slug, }); // similarity-embeddings feature is only available on project details const hasSimilarityEmbeddingsFeature = projectData?.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(() => { if (isPending) { return; } setStatus('loading'); const reqs: Parameters[0] = []; if (hasSimilarityEmbeddingsFeature) { reqs.push({ endpoint: `/organizations/${organization.slug}/issues/${params.groupId}/similar-issues-embeddings/?${qs.stringify( { k: 10, threshold: 0.01, useReranking, } )}`, dataKey: 'similar', }); } else if (hasSimilarityFeature) { reqs.push({ endpoint: `/organizations/${organization.slug}/issues/${params.groupId}/similar/?${qs.stringify( { ...location.query, limit: 50, } )}`, dataKey: 'similar', }); } GroupingStore.onFetch(reqs); }, [ location.query, params.groupId, organization.slug, hasSimilarityFeature, hasSimilarityEmbeddingsFeature, useReranking, isPending, ]); 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 !== params.groupId) { // Merge success, since we can't specify target, we need to redirect to new parent navigate(`/organizations/${organization.slug}/issues/${mergedParent}/similar/`); } }, [navigate, params.groupId, organization.slug] ); 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.query as string, projectId: firstIssue.issue.project.slug, }); }, [params, location.query, items]); const hasSimilarItems = (hasSimilarityFeature || hasSimilarityEmbeddingsFeature) && (items.similar.length > 0 || items.filtered.length > 0); return ( {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')` font-size: ${p => p.theme.fontSizeLarge}; margin-bottom: ${space(0.75)}; `; const HeaderWrapper = styled('div')` margin-bottom: ${space(2)}; small { color: ${p => p.theme.subText}; } `;