index.tsx 5.0 KB

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