traceIssue.tsx 5.2 KB

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