index.tsx 7.6 KB

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