index.tsx 7.9 KB

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