traceIssue.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {useAnalyticsArea} from 'sentry/components/analyticsArea';
  5. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  6. import Link from 'sentry/components/links/link';
  7. import Placeholder from 'sentry/components/placeholder';
  8. import {space} from 'sentry/styles/space';
  9. import {trackAnalytics} from 'sentry/utils/analytics';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import useProjectFromSlug from 'sentry/utils/useProjectFromSlug';
  12. import type {TimelineEvent} from './useTraceTimelineEvents';
  13. interface TraceIssueEventProps {
  14. event: TimelineEvent;
  15. }
  16. export function TraceIssueEvent({event}: TraceIssueEventProps) {
  17. const organization = useOrganization();
  18. const project = useProjectFromSlug({organization, projectSlug: event['project.name']});
  19. const issueId = event['issue.id'];
  20. const {title, subtitle, message} = getTitleSubtitleMessage(event);
  21. const avatarSize = parseInt(space(4), 10);
  22. const area = useAnalyticsArea();
  23. // Referrer used to be hard-coded for this component. It's used for analytics
  24. // only. We'll still use the old referrer for backwards compatibility.
  25. const queryReferrer = area.includes('issue_details')
  26. ? 'issue_details.related_trace_issue'
  27. : area;
  28. return (
  29. <Fragment>
  30. <TraceIssueLinkContainer
  31. to={{
  32. pathname: `/organizations/${organization.slug}/issues/${issueId}/events/${event.id}/`,
  33. query: {
  34. referrer: queryReferrer,
  35. },
  36. }}
  37. onClick={() => {
  38. // Track this event for backwards compatibility. TODO: remove after issues team dashboards/queries are migrated
  39. if (area.includes('issue_details')) {
  40. trackAnalytics('issue_details.related_trace_issue.trace_issue_clicked', {
  41. organization,
  42. group_id: issueId,
  43. });
  44. }
  45. trackAnalytics('one_other_related_trace_issue.clicked', {
  46. organization,
  47. group_id: issueId,
  48. area: area,
  49. });
  50. }}
  51. >
  52. <TraceIssueProjectBadge>
  53. {project ? (
  54. <ProjectBadge
  55. project={project}
  56. avatarSize={avatarSize}
  57. hideName
  58. disableLink
  59. />
  60. ) : (
  61. <Placeholder
  62. shape="rect"
  63. width={`${projectBadgeSize}px`}
  64. height={`${projectBadgeSize}px`}
  65. />
  66. )}
  67. </TraceIssueProjectBadge>
  68. <TraceIssueDetailsContainer>
  69. <NoOverflowDiv>
  70. <TraceIssueEventTitle>{title}</TraceIssueEventTitle>
  71. <TraceIssueEventSubtitle data-test-id="subtitle-span">
  72. {subtitle}
  73. </TraceIssueEventSubtitle>
  74. </NoOverflowDiv>
  75. <NoOverflowDiv>{message}</NoOverflowDiv>
  76. </TraceIssueDetailsContainer>
  77. </TraceIssueLinkContainer>
  78. </Fragment>
  79. );
  80. }
  81. // This function tries to imitate what getTitle() from utils.events does.
  82. // In that module, the data comes from the issues endpoint while in here
  83. // we grab the data from the events endpoint. A larger effort is
  84. // required in order to use that function directly since the data between
  85. // the two endpoint is slightly different.
  86. // For instance, the events endpoint could include a _metadata dict with
  87. // the title, subtitle and message.
  88. // We could also make another call to the issues endpoint to fetch the metadata,
  89. // however, we currently don't support it and it is extremely slow
  90. export function getTitleSubtitleMessage(event: TimelineEvent) {
  91. let title = event.title.trimEnd();
  92. let subtitle = event.culprit;
  93. let message = '';
  94. try {
  95. if (event['event.type'] === 'error') {
  96. if (title[title.length - 1] !== ':') {
  97. title = event.title.split(':')[0];
  98. }
  99. // It uses metadata.value which could differ depending on what error.value is used in the event manager
  100. // TODO: Add support for chained exceptions since we grab the value from the first stack trace
  101. // https://github.com/getsentry/sentry/blob/a221f399d2b4190f2631fcca311bdb5b3748838b/src/sentry/eventtypes/error.py#L115-L134
  102. message = event['error.value'].at(-1) ?? '';
  103. } else if (event['event.type'] === 'default') {
  104. // See getTitle() and getMessage() in sentry/utils/events.tsx
  105. subtitle = '';
  106. message = event.culprit;
  107. } else {
  108. // It is suspected that this value is calculated somewhere in Relay
  109. // and we deconstruct it here to match what the Issue details page shows
  110. message = event.message.replace(event.culprit, '').replace(title, '').trimStart();
  111. }
  112. } catch (error) {
  113. // If we fail, report it so we can figure it out
  114. Sentry.captureException(error);
  115. }
  116. return {title, subtitle, message};
  117. }
  118. const TraceIssueLinkContainer = styled(Link)`
  119. display: flex;
  120. gap: ${space(2)};
  121. color: ${p => p.theme.textColor};
  122. padding: ${space(2)} ${space(2)} ${space(2)} ${space(2)};
  123. margin: ${space(1)} 0 ${space(1)} 0;
  124. border: 1px solid ${p => p.theme.border};
  125. border-radius: ${p => p.theme.borderRadius};
  126. font-size: ${p => p.theme.fontSizeMedium};
  127. &:hover {
  128. background-color: ${p => p.theme.backgroundTertiary};
  129. color: ${p => p.theme.textColor};
  130. }
  131. `;
  132. // This size helps line up the contents of Suspect Commit
  133. // with the project avatar
  134. const projectBadgeSize = 36;
  135. const TraceIssueProjectBadge = styled('div')`
  136. height: ${projectBadgeSize}px;
  137. width: ${projectBadgeSize}px;
  138. min-width: ${projectBadgeSize}px;
  139. display: flex;
  140. align-self: center;
  141. justify-content: center;
  142. `;
  143. const TraceIssueDetailsContainer = styled('div')`
  144. ${p => p.theme.overflowEllipsis};
  145. `;
  146. const NoOverflowDiv = styled('div')`
  147. ${p => p.theme.overflowEllipsis};
  148. `;
  149. const TraceIssueEventTitle = styled('span')`
  150. font-weight: 600;
  151. margin-right: ${space(1)};
  152. `;
  153. const TraceIssueEventSubtitle = styled('span')`
  154. color: ${p => p.theme.subText};
  155. `;