index.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import {useCallback, useEffect, useState} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import * as qs from 'query-string';
  6. import {Alert} from 'sentry/components/alert';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import IssuesReplayCountProvider from 'sentry/components/replays/issuesReplayCountProvider';
  11. import {SegmentedControl} from 'sentry/components/segmentedControl';
  12. import {t} from 'sentry/locale';
  13. import GroupingStore, {SimilarItem} from 'sentry/stores/groupingStore';
  14. import {space} from 'sentry/styles/space';
  15. import {Project} from 'sentry/types';
  16. import {useNavigate} from 'sentry/utils/useNavigate';
  17. import usePrevious from 'sentry/utils/usePrevious';
  18. import List from './list';
  19. type RouteParams = {
  20. groupId: string;
  21. orgId: string;
  22. };
  23. type Props = RouteComponentProps<RouteParams, {}> & {
  24. location: Location;
  25. project: Project;
  26. };
  27. type ItemState = {
  28. filtered: SimilarItem[];
  29. pageLinks: string | null;
  30. similar: SimilarItem[];
  31. };
  32. function SimilarStackTrace({params, location, project}: Props) {
  33. const {orgId, groupId} = params;
  34. const [isUsingSimilarityViewV2, setIsUsingSimilarityViewV2] = useState<boolean>(false);
  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 fetchData = useCallback(() => {
  45. setStatus('loading');
  46. const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
  47. if (hasSimilarityFeature) {
  48. const version = isUsingSimilarityViewV2 ? '2' : '1';
  49. reqs.push({
  50. endpoint: `/issues/${groupId}/similar/?${qs.stringify({
  51. ...location.query,
  52. limit: 50,
  53. version,
  54. })}`,
  55. dataKey: 'similar',
  56. });
  57. }
  58. GroupingStore.onFetch(reqs);
  59. }, [location.query, groupId, isUsingSimilarityViewV2, hasSimilarityFeature]);
  60. const onGroupingChange = useCallback(
  61. ({
  62. mergedParent,
  63. similarItems: updatedSimilarItems,
  64. filteredSimilarItems: updatedFilteredSimilarItems,
  65. similarLinks: updatedSimilarLinks,
  66. loading,
  67. error,
  68. }) => {
  69. if (updatedSimilarItems) {
  70. setItems({
  71. similar: updatedSimilarItems,
  72. filtered: updatedFilteredSimilarItems,
  73. pageLinks: updatedSimilarLinks,
  74. });
  75. setStatus(error ? 'error' : loading ? 'loading' : 'ready');
  76. return;
  77. }
  78. if (mergedParent && mergedParent !== groupId) {
  79. // Merge success, since we can't specify target, we need to redirect to new parent
  80. navigate(`/organizations/${orgId}/issues/${mergedParent}/similar/`);
  81. }
  82. },
  83. [navigate, groupId, orgId]
  84. );
  85. useEffect(() => {
  86. fetchData();
  87. }, [fetchData]);
  88. useEffect(() => {
  89. if (prevLocationSearch !== location.search) {
  90. fetchData();
  91. }
  92. }, [fetchData, prevLocationSearch, location.search]);
  93. useEffect(() => {
  94. const unsubscribe = GroupingStore.listen(onGroupingChange, undefined);
  95. return () => {
  96. unsubscribe();
  97. };
  98. }, [onGroupingChange]);
  99. const handleMerge = useCallback(() => {
  100. if (!params) {
  101. return;
  102. }
  103. // You need at least 1 similarItem OR filteredSimilarItems to be able to merge,
  104. // so `firstIssue` should always exist from one of those lists.
  105. //
  106. // Similar issues API currently does not return issues across projects,
  107. // so we can assume that the first issues project slug is the project in
  108. // scope
  109. const [firstIssue] = items.similar.length ? items.similar : items.filtered;
  110. GroupingStore.onMerge({
  111. params,
  112. query: location.query,
  113. projectId: firstIssue.issue.project.slug,
  114. });
  115. }, [params, location.query, items]);
  116. const hasSimilarityViewV2 = project.features.includes('similarity-view-v2');
  117. const hasSimilarItems =
  118. hasSimilarityFeature &&
  119. (items.similar.length > 0 || items.filtered.length > 0) &&
  120. status === 'ready';
  121. const groupsIds = items.similar.concat(items.filtered).map(({issue}) => issue.id);
  122. return (
  123. <Layout.Body>
  124. <Layout.Main fullWidth>
  125. <Alert type="warning">
  126. {t(
  127. 'This is an experimental feature. Data may not be immediately available while we process merges.'
  128. )}
  129. </Alert>
  130. <HeaderWrapper>
  131. <Title>{t('Issues with a similar stack trace')}</Title>
  132. {hasSimilarityViewV2 && (
  133. <SegmentedControl
  134. aria-label={t('Algorithm')}
  135. size="sm"
  136. value={isUsingSimilarityViewV2 ? 'new' : 'old'}
  137. onChange={key => setIsUsingSimilarityViewV2(key === 'new')}
  138. >
  139. <SegmentedControl.Item key="old">
  140. {t('Old Algorithm')}
  141. </SegmentedControl.Item>
  142. <SegmentedControl.Item key="new">
  143. {t('New Algorithm')}
  144. </SegmentedControl.Item>
  145. </SegmentedControl>
  146. )}
  147. </HeaderWrapper>
  148. {status === 'loading' && <LoadingIndicator />}
  149. {status === 'error' && (
  150. <LoadingError
  151. message={t('Unable to load similar issues, please try again later')}
  152. onRetry={fetchData}
  153. />
  154. )}
  155. {hasSimilarItems && (
  156. <IssuesReplayCountProvider groupIds={groupsIds}>
  157. <List
  158. items={items.similar}
  159. filteredItems={items.filtered}
  160. onMerge={handleMerge}
  161. orgId={orgId}
  162. project={project}
  163. groupId={groupId}
  164. pageLinks={items.pageLinks}
  165. v2={isUsingSimilarityViewV2}
  166. />
  167. </IssuesReplayCountProvider>
  168. )}
  169. </Layout.Main>
  170. </Layout.Body>
  171. );
  172. }
  173. export default SimilarStackTrace;
  174. const Title = styled('h4')`
  175. margin-bottom: 0;
  176. `;
  177. const HeaderWrapper = styled('div')`
  178. display: flex;
  179. align-items: center;
  180. justify-content: space-between;
  181. margin-bottom: ${space(2)};
  182. `;