index.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {LocationDescriptor} from 'history';
  4. import {LinkButton} from 'sentry/components/button';
  5. import GroupList from 'sentry/components/issues/groupList';
  6. import Link from 'sentry/components/links/link';
  7. import LoadingError from 'sentry/components/loadingError';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {t, tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Group} from 'sentry/types/group';
  12. import type {Organization} from 'sentry/types/organization';
  13. import {useApiQuery} from 'sentry/utils/queryClient';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. type RelatedIssuesResponse = {
  16. data: number[];
  17. meta: {
  18. event_id: string;
  19. trace_id: string;
  20. };
  21. type: string;
  22. };
  23. function GroupRelatedIssues({group}: {group: Group}) {
  24. return (
  25. <Fragment>
  26. <RelatedIssuesSection group={group} relationType="same_root_cause" />
  27. <RelatedIssuesSection group={group} relationType="trace_connected" />
  28. </Fragment>
  29. );
  30. }
  31. interface RelatedIssuesSectionProps {
  32. group: Group;
  33. relationType: string;
  34. }
  35. function RelatedIssuesSection({group, relationType}: RelatedIssuesSectionProps) {
  36. const organization = useOrganization();
  37. // Fetch the list of related issues
  38. const hasGlobalViewsFeature = organization.features.includes('global-views');
  39. const {
  40. isPending,
  41. isError,
  42. data: relatedIssues,
  43. refetch,
  44. } = useApiQuery<RelatedIssuesResponse>(
  45. [
  46. `/issues/${group.id}/related-issues/`,
  47. {
  48. query: {
  49. ...(hasGlobalViewsFeature ? undefined : {project: group.project.id}),
  50. type: relationType,
  51. },
  52. },
  53. ],
  54. {
  55. staleTime: 0,
  56. }
  57. );
  58. const traceMeta = relationType === 'trace_connected' ? relatedIssues?.meta : undefined;
  59. const issues = relatedIssues?.data ?? [];
  60. const query = `issue.id:[${issues}]`;
  61. let title: React.ReactNode = null;
  62. let extraInfo: React.ReactNode = null;
  63. let openIssuesButton: React.ReactNode = null;
  64. if (relationType === 'trace_connected' && traceMeta) {
  65. ({title, extraInfo, openIssuesButton} = getTraceConnectedContent(
  66. traceMeta,
  67. organization,
  68. group
  69. ));
  70. } else {
  71. title = t('Issues with similar titles');
  72. extraInfo = t(
  73. 'These issues have the same title and may have been caused by the same root cause.'
  74. );
  75. openIssuesButton = getLinkButton(
  76. {
  77. pathname: `/organizations/${organization.slug}/issues/`,
  78. query: {
  79. // project=-1 allows ensuring that the query will show issues from any projects for the org
  80. // This is important for traces since issues can be for any project in the org
  81. ...(hasGlobalViewsFeature ? {project: '-1'} : {project: group.project.id}),
  82. query: `issue.id:[${group.id},${issues}]`,
  83. },
  84. },
  85. 'Clicked Open Issues from same-root related issues',
  86. 'similar_issues.same_root_cause_clicked_open_issues'
  87. );
  88. }
  89. return (
  90. <Fragment>
  91. {isPending ? (
  92. <LoadingIndicator />
  93. ) : isError ? (
  94. <LoadingError
  95. message={t('Unable to load related issues, please try again later')}
  96. onRetry={refetch}
  97. />
  98. ) : issues.length > 0 ? (
  99. <Fragment>
  100. <HeaderWrapper>
  101. <Title>{title}</Title>
  102. <TextButtonWrapper>
  103. <span>{extraInfo}</span>
  104. {openIssuesButton}
  105. </TextButtonWrapper>
  106. </HeaderWrapper>
  107. <GroupList
  108. orgSlug={organization.slug}
  109. queryParams={{
  110. query,
  111. ...(hasGlobalViewsFeature ? undefined : {project: group.project.id}),
  112. }}
  113. source="similar-issues-tab"
  114. canSelectGroups={false}
  115. withChart={false}
  116. withColumns={['event']}
  117. />
  118. </Fragment>
  119. ) : null}
  120. </Fragment>
  121. );
  122. }
  123. const getTraceConnectedContent = (
  124. traceMeta: RelatedIssuesResponse['meta'],
  125. organization: Organization,
  126. group: Group
  127. ) => {
  128. const hasGlobalViewsFeature = organization.features.includes('global-views');
  129. const title = t('Issues in the same trace');
  130. const url = `/organizations/${organization.slug}/performance/trace/${traceMeta.trace_id}/?node=error-${traceMeta.event_id}`;
  131. const extraInfo = (
  132. <small>
  133. {tct('These issues were all found within [traceLink:this trace]', {
  134. traceLink: <Link to={url}>{t('this trace')}</Link>,
  135. })}
  136. </small>
  137. );
  138. const openIssuesButton = getLinkButton(
  139. {
  140. pathname: `/organizations/${organization.slug}/issues/`,
  141. query: {
  142. // project=-1 allows ensuring that the query will show issues from any projects for the org
  143. // This is important for traces since issues can be for any project in the org
  144. ...(hasGlobalViewsFeature ? {project: '-1'} : {project: group.project.id}),
  145. query: `trace:${traceMeta.trace_id}`,
  146. },
  147. },
  148. 'Clicked Open Issues from trace-connected related issues',
  149. 'similar_issues.trace_connected_issues_clicked_open_issues'
  150. );
  151. return {title, extraInfo, openIssuesButton};
  152. };
  153. const getLinkButton = (to: LocationDescriptor, eventName: string, eventKey: string) => {
  154. return (
  155. <LinkButton
  156. to={to}
  157. size="xs"
  158. analyticsEventName={eventName}
  159. analyticsEventKey={eventKey}
  160. >
  161. {t('Open in Issues')}
  162. </LinkButton>
  163. );
  164. };
  165. // Export the component without feature flag controls
  166. export {GroupRelatedIssues};
  167. const Title = styled('h4')`
  168. font-size: ${p => p.theme.fontSizeLarge};
  169. margin-bottom: ${space(0.75)};
  170. `;
  171. const HeaderWrapper = styled('div')`
  172. margin-bottom: ${space(2)};
  173. small {
  174. color: ${p => p.theme.subText};
  175. }
  176. `;
  177. const TextButtonWrapper = styled('div')`
  178. align-items: center;
  179. display: flex;
  180. justify-content: space-between;
  181. margin-bottom: ${space(1)};
  182. width: 100%;
  183. `;