123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- 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<RouteParams, {}> & {
- 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<ItemState>({
- 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<typeof GroupingStore.onFetch>[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 (
- <Fragment>
- {hasSimilarityEmbeddingsFeature && (
- <Alert
- type="info"
- showIcon
- defaultExpanded
- expand={
- 'We\'d love to get your feedback on the accuracy of this score. You can check off individuals rows with "Agree" and "Disagree" to send us feedback on how you\'d classify each decision we\'ve made.'
- }
- >
- 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.
- </Alert>
- )}
- <HeaderWrapper>
- <Title>{t('Issues with a similar stack trace')}</Title>
- <small>
- {t(
- 'This is an experimental feature. Data may not be immediately available while we process merges.'
- )}
- </small>
- </HeaderWrapper>
- {status === 'loading' && <LoadingIndicator />}
- {status === 'error' && (
- <LoadingError
- message={t('Unable to load similar issues, please try again later')}
- onRetry={fetchData}
- />
- )}
- {status === 'ready' && !hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
- <Panel>
- <EmptyStateWarning>
- <p>{t("There don't seem to be any similar issues.")}</p>
- </EmptyStateWarning>
- </Panel>
- )}
- {status === 'ready' && !hasSimilarItems && hasSimilarityEmbeddingsFeature && (
- <Panel>
- <EmptyStateWarning>
- <p>
- {t(
- "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
- )}
- </p>
- </EmptyStateWarning>
- </Panel>
- )}
- {status === 'ready' && hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
- <List
- items={items.similar}
- filteredItems={items.filtered}
- onMerge={handleMerge}
- orgId={orgId}
- project={project}
- groupId={groupId}
- pageLinks={items.pageLinks}
- location={location}
- />
- )}
- {status === 'ready' && hasSimilarItems && hasSimilarityEmbeddingsFeature && (
- <List
- items={items.similar.concat(items.filtered)}
- filteredItems={[]}
- onMerge={handleMerge}
- orgId={orgId}
- project={project}
- groupId={groupId}
- pageLinks={items.pageLinks}
- location={location}
- />
- )}
- <DataConsentBanner source="grouping" />
- </Fragment>
- );
- }
- 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};
- }
- `;
|