index.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import * as qs from 'query-string';
  6. import Alert from 'sentry/components/alert';
  7. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  8. import HookOrDefault from 'sentry/components/hookOrDefault';
  9. import LoadingError from 'sentry/components/loadingError';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import Panel from 'sentry/components/panels/panel';
  12. import {t} from 'sentry/locale';
  13. import type {SimilarItem} from 'sentry/stores/groupingStore';
  14. import GroupingStore from 'sentry/stores/groupingStore';
  15. import {space} from 'sentry/styles/space';
  16. import type {Project} from 'sentry/types/project';
  17. import {useNavigate} from 'sentry/utils/useNavigate';
  18. import usePrevious from 'sentry/utils/usePrevious';
  19. import List from './list';
  20. type RouteParams = {
  21. groupId: string;
  22. orgId: string;
  23. };
  24. type Props = RouteComponentProps<RouteParams, {}> & {
  25. location: Location;
  26. project: Project;
  27. };
  28. type ItemState = {
  29. filtered: SimilarItem[];
  30. pageLinks: string | null;
  31. similar: SimilarItem[];
  32. };
  33. const DataConsentBanner = HookOrDefault({
  34. hookName: 'component:data-consent-banner',
  35. defaultComponent: null,
  36. });
  37. function SimilarStackTrace({params, location, project}: Props) {
  38. const {orgId, groupId} = params;
  39. const [items, setItems] = useState<ItemState>({
  40. similar: [],
  41. filtered: [],
  42. pageLinks: null,
  43. });
  44. const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading');
  45. const navigate = useNavigate();
  46. const prevLocationSearch = usePrevious(location.search);
  47. const hasSimilarityFeature = project.features.includes('similarity-view');
  48. const hasSimilarityEmbeddingsFeature =
  49. project.features.includes('similarity-embeddings') ||
  50. location.query.similarityEmbeddings === '1';
  51. // Use reranking by default (assuming the `seer.similarity.similar_issues.use_reranking`
  52. // backend option is using its default value of `True`). This is just so we can turn it off
  53. // on demand to see if/how that changes the results.
  54. const useReranking = String(location.query.useReranking !== '0');
  55. const fetchData = useCallback(() => {
  56. setStatus('loading');
  57. const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
  58. if (hasSimilarityEmbeddingsFeature) {
  59. reqs.push({
  60. endpoint: `/organizations/${orgId}/issues/${groupId}/similar-issues-embeddings/?${qs.stringify(
  61. {
  62. k: 10,
  63. threshold: 0.01,
  64. useReranking,
  65. }
  66. )}`,
  67. dataKey: 'similar',
  68. });
  69. } else if (hasSimilarityFeature) {
  70. reqs.push({
  71. endpoint: `/organizations/${orgId}/issues/${groupId}/similar/?${qs.stringify({
  72. ...location.query,
  73. limit: 50,
  74. })}`,
  75. dataKey: 'similar',
  76. });
  77. }
  78. GroupingStore.onFetch(reqs);
  79. }, [
  80. location.query,
  81. groupId,
  82. orgId,
  83. hasSimilarityFeature,
  84. hasSimilarityEmbeddingsFeature,
  85. useReranking,
  86. ]);
  87. const onGroupingChange = useCallback(
  88. ({
  89. mergedParent,
  90. similarItems: updatedSimilarItems,
  91. filteredSimilarItems: updatedFilteredSimilarItems,
  92. similarLinks: updatedSimilarLinks,
  93. loading,
  94. error,
  95. }) => {
  96. if (updatedSimilarItems) {
  97. setItems({
  98. similar: updatedSimilarItems,
  99. filtered: updatedFilteredSimilarItems,
  100. pageLinks: updatedSimilarLinks,
  101. });
  102. setStatus(error ? 'error' : loading ? 'loading' : 'ready');
  103. return;
  104. }
  105. if (mergedParent && mergedParent !== groupId) {
  106. // Merge success, since we can't specify target, we need to redirect to new parent
  107. navigate(`/organizations/${orgId}/issues/${mergedParent}/similar/`);
  108. }
  109. },
  110. [navigate, groupId, orgId]
  111. );
  112. useEffect(() => {
  113. fetchData();
  114. }, [fetchData]);
  115. useEffect(() => {
  116. if (prevLocationSearch !== location.search) {
  117. fetchData();
  118. }
  119. }, [fetchData, prevLocationSearch, location.search]);
  120. useEffect(() => {
  121. const unsubscribe = GroupingStore.listen(onGroupingChange, undefined);
  122. return () => {
  123. unsubscribe();
  124. };
  125. }, [onGroupingChange]);
  126. const handleMerge = useCallback(() => {
  127. if (!params) {
  128. return;
  129. }
  130. // You need at least 1 similarItem OR filteredSimilarItems to be able to merge,
  131. // so `firstIssue` should always exist from one of those lists.
  132. //
  133. // Similar issues API currently does not return issues across projects,
  134. // so we can assume that the first issues project slug is the project in
  135. // scope
  136. const [firstIssue] = items.similar.length ? items.similar : items.filtered;
  137. GroupingStore.onMerge({
  138. params,
  139. query: location.query,
  140. projectId: firstIssue.issue.project.slug,
  141. });
  142. }, [params, location.query, items]);
  143. const hasSimilarItems =
  144. (hasSimilarityFeature || hasSimilarityEmbeddingsFeature) &&
  145. (items.similar.length > 0 || items.filtered.length > 0);
  146. return (
  147. <Fragment>
  148. {hasSimilarityEmbeddingsFeature && (
  149. <Alert
  150. type="info"
  151. showIcon
  152. defaultExpanded
  153. expand={
  154. '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.'
  155. }
  156. >
  157. Hi there! We're working on improving grouping with ML techniques. Each similar
  158. issue has been scored as "Would Group: Yes" and "Would Group: No," which refers
  159. to whether or not we'd group the similar issue into the main issue.
  160. </Alert>
  161. )}
  162. <HeaderWrapper>
  163. <Title>{t('Issues with a similar stack trace')}</Title>
  164. <small>
  165. {t(
  166. 'This is an experimental feature. Data may not be immediately available while we process merges.'
  167. )}
  168. </small>
  169. </HeaderWrapper>
  170. {status === 'loading' && <LoadingIndicator />}
  171. {status === 'error' && (
  172. <LoadingError
  173. message={t('Unable to load similar issues, please try again later')}
  174. onRetry={fetchData}
  175. />
  176. )}
  177. {status === 'ready' && !hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
  178. <Panel>
  179. <EmptyStateWarning>
  180. <p>{t("There don't seem to be any similar issues.")}</p>
  181. </EmptyStateWarning>
  182. </Panel>
  183. )}
  184. {status === 'ready' && !hasSimilarItems && hasSimilarityEmbeddingsFeature && (
  185. <Panel>
  186. <EmptyStateWarning>
  187. <p>
  188. {t(
  189. "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
  190. )}
  191. </p>
  192. </EmptyStateWarning>
  193. </Panel>
  194. )}
  195. {status === 'ready' && hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
  196. <List
  197. items={items.similar}
  198. filteredItems={items.filtered}
  199. onMerge={handleMerge}
  200. orgId={orgId}
  201. project={project}
  202. groupId={groupId}
  203. pageLinks={items.pageLinks}
  204. location={location}
  205. />
  206. )}
  207. {status === 'ready' && hasSimilarItems && hasSimilarityEmbeddingsFeature && (
  208. <List
  209. items={items.similar.concat(items.filtered)}
  210. filteredItems={[]}
  211. onMerge={handleMerge}
  212. orgId={orgId}
  213. project={project}
  214. groupId={groupId}
  215. pageLinks={items.pageLinks}
  216. location={location}
  217. />
  218. )}
  219. <DataConsentBanner source="grouping" />
  220. </Fragment>
  221. );
  222. }
  223. export default SimilarStackTrace;
  224. const Title = styled('h4')`
  225. margin-bottom: ${space(0.75)};
  226. `;
  227. const HeaderWrapper = styled('div')`
  228. margin-bottom: ${space(2)};
  229. small {
  230. color: ${p => p.theme.subText};
  231. }
  232. `;