index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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 * as Layout from 'sentry/components/layouts/thirds';
  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';
  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. function SimilarStackTrace({params, location, project}: Props) {
  34. const {orgId, groupId} = params;
  35. const [items, setItems] = useState<ItemState>({
  36. similar: [],
  37. filtered: [],
  38. pageLinks: null,
  39. });
  40. const [status, setStatus] = useState<'loading' | 'error' | 'ready'>('loading');
  41. const navigate = useNavigate();
  42. const prevLocationSearch = usePrevious(location.search);
  43. const hasSimilarityFeature = project.features.includes('similarity-view');
  44. const hasSimilarityEmbeddingsFeature = project.features.includes(
  45. 'similarity-embeddings'
  46. );
  47. const fetchData = useCallback(() => {
  48. setStatus('loading');
  49. const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
  50. if (hasSimilarityEmbeddingsFeature) {
  51. reqs.push({
  52. endpoint: `/organizations/${orgId}/issues/${groupId}/similar-issues-embeddings/?${qs.stringify(
  53. {
  54. k: 10,
  55. threshold: 0.01,
  56. }
  57. )}`,
  58. dataKey: 'similar',
  59. });
  60. } else if (hasSimilarityFeature) {
  61. reqs.push({
  62. endpoint: `/organizations/${orgId}/issues/${groupId}/similar/?${qs.stringify({
  63. ...location.query,
  64. limit: 50,
  65. })}`,
  66. dataKey: 'similar',
  67. });
  68. }
  69. GroupingStore.onFetch(reqs);
  70. }, [
  71. location.query,
  72. groupId,
  73. orgId,
  74. hasSimilarityFeature,
  75. hasSimilarityEmbeddingsFeature,
  76. ]);
  77. const onGroupingChange = useCallback(
  78. ({
  79. mergedParent,
  80. similarItems: updatedSimilarItems,
  81. filteredSimilarItems: updatedFilteredSimilarItems,
  82. similarLinks: updatedSimilarLinks,
  83. loading,
  84. error,
  85. }) => {
  86. if (updatedSimilarItems) {
  87. setItems({
  88. similar: updatedSimilarItems,
  89. filtered: updatedFilteredSimilarItems,
  90. pageLinks: updatedSimilarLinks,
  91. });
  92. setStatus(error ? 'error' : loading ? 'loading' : 'ready');
  93. return;
  94. }
  95. if (mergedParent && mergedParent !== groupId) {
  96. // Merge success, since we can't specify target, we need to redirect to new parent
  97. navigate(`/organizations/${orgId}/issues/${mergedParent}/similar/`);
  98. }
  99. },
  100. [navigate, groupId, orgId]
  101. );
  102. useEffect(() => {
  103. fetchData();
  104. }, [fetchData]);
  105. useEffect(() => {
  106. if (prevLocationSearch !== location.search) {
  107. fetchData();
  108. }
  109. }, [fetchData, prevLocationSearch, location.search]);
  110. useEffect(() => {
  111. const unsubscribe = GroupingStore.listen(onGroupingChange, undefined);
  112. return () => {
  113. unsubscribe();
  114. };
  115. }, [onGroupingChange]);
  116. const handleMerge = useCallback(() => {
  117. if (!params) {
  118. return;
  119. }
  120. // You need at least 1 similarItem OR filteredSimilarItems to be able to merge,
  121. // so `firstIssue` should always exist from one of those lists.
  122. //
  123. // Similar issues API currently does not return issues across projects,
  124. // so we can assume that the first issues project slug is the project in
  125. // scope
  126. const [firstIssue] = items.similar.length ? items.similar : items.filtered;
  127. GroupingStore.onMerge({
  128. params,
  129. query: location.query,
  130. projectId: firstIssue.issue.project.slug,
  131. });
  132. }, [params, location.query, items]);
  133. const hasSimilarItems =
  134. (hasSimilarityFeature || hasSimilarityEmbeddingsFeature) &&
  135. (items.similar.length > 0 || items.filtered.length > 0);
  136. return (
  137. <Fragment>
  138. {hasSimilarityEmbeddingsFeature && (
  139. <Alert
  140. type="info"
  141. showIcon
  142. defaultExpanded
  143. expand={
  144. '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. If you have any questions, you can feel free to reach out to the team at #proj-ml-grouping.'
  145. }
  146. >
  147. Hi there! We're running an internal POC to improve grouping with ML techniques.
  148. Each similar issue has been scored as "Would Group: Yes" and "Would Group: No,"
  149. which refers to whether or not we'd group the similar issue into the main issue.
  150. </Alert>
  151. )}
  152. <Layout.Body>
  153. <Layout.Main fullWidth>
  154. <HeaderWrapper>
  155. <Title>{t('Issues with a similar stack trace')}</Title>
  156. <small>
  157. {t(
  158. 'This is an experimental feature. Data may not be immediately available while we process merges.'
  159. )}
  160. </small>
  161. </HeaderWrapper>
  162. {status === 'loading' && <LoadingIndicator />}
  163. {status === 'error' && (
  164. <LoadingError
  165. message={t('Unable to load similar issues, please try again later')}
  166. onRetry={fetchData}
  167. />
  168. )}
  169. {status === 'ready' && !hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
  170. <Panel>
  171. <EmptyStateWarning>
  172. <p>{t("There don't seem to be any similar issues.")}</p>
  173. </EmptyStateWarning>
  174. </Panel>
  175. )}
  176. {status === 'ready' && !hasSimilarItems && hasSimilarityEmbeddingsFeature && (
  177. <Panel>
  178. <EmptyStateWarning>
  179. <p>
  180. {t(
  181. "There don't seem to be any similar issues. This can occur when the issue has no stacktrace or in-app frames."
  182. )}
  183. </p>
  184. </EmptyStateWarning>
  185. </Panel>
  186. )}
  187. {status === 'ready' && hasSimilarItems && !hasSimilarityEmbeddingsFeature && (
  188. <List
  189. items={items.similar}
  190. filteredItems={items.filtered}
  191. onMerge={handleMerge}
  192. orgId={orgId}
  193. project={project}
  194. groupId={groupId}
  195. pageLinks={items.pageLinks}
  196. />
  197. )}
  198. {status === 'ready' && hasSimilarItems && hasSimilarityEmbeddingsFeature && (
  199. <List
  200. items={items.similar.concat(items.filtered)}
  201. filteredItems={[]}
  202. onMerge={handleMerge}
  203. orgId={orgId}
  204. project={project}
  205. groupId={groupId}
  206. pageLinks={items.pageLinks}
  207. />
  208. )}
  209. </Layout.Main>
  210. </Layout.Body>
  211. </Fragment>
  212. );
  213. }
  214. export default SimilarStackTrace;
  215. const Title = styled('h4')`
  216. margin-bottom: ${space(0.75)};
  217. `;
  218. const HeaderWrapper = styled('div')`
  219. margin-bottom: ${space(2)};
  220. small {
  221. color: ${p => p.theme.subText};
  222. }
  223. `;