traceTimeline.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import {useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import Feature from 'sentry/components/acl/feature';
  4. import ErrorBoundary from 'sentry/components/errorBoundary';
  5. import QuestionTooltip from 'sentry/components/questionTooltip';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Event} from 'sentry/types/event';
  9. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  10. import {useDimensions} from 'sentry/utils/useDimensions';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {TraceTimelineEvents} from './traceTimelineEvents';
  13. import {EventItem} from './traceTimelineTooltip';
  14. import {type TimelineEvent, useTraceTimelineEvents} from './useTraceTimelineEvents';
  15. const ISSUES_TO_SKIP_TIMELINE = 2;
  16. interface TraceTimelineProps {
  17. event: Event;
  18. }
  19. export function TraceTimeline({event}: TraceTimelineProps) {
  20. const organization = useOrganization();
  21. const timelineRef = useRef<HTMLDivElement>(null);
  22. const {width} = useDimensions({elementRef: timelineRef});
  23. const {isError, isLoading, traceEvents} = useTraceTimelineEvents({event});
  24. const hasTraceId = !!event.contexts?.trace?.trace_id;
  25. let timelineStatus: string | undefined = 'empty';
  26. let timelineSkipped = false;
  27. let issuesCount = 0;
  28. if (hasTraceId && !isLoading) {
  29. if (!organization.features.includes('related-issues-issue-details-page')) {
  30. timelineStatus = traceEvents.length > 1 ? 'shown' : 'empty';
  31. } else {
  32. issuesCount = getIssuesCountFromEvents(traceEvents);
  33. // When we have more than 2 issues regardless of the number of events we skip the timeline
  34. timelineSkipped = issuesCount === ISSUES_TO_SKIP_TIMELINE;
  35. timelineStatus = timelineSkipped ? 'empty' : 'shown';
  36. }
  37. } else if (!hasTraceId) {
  38. timelineStatus = 'no_trace_id';
  39. }
  40. useRouteAnalyticsParams(timelineStatus ? {trace_timeline_status: timelineStatus} : {});
  41. if (!hasTraceId) {
  42. return null;
  43. }
  44. const noEvents = !isLoading && traceEvents.length === 0;
  45. // Timelines with only the current event are not useful
  46. const onlySelfEvent =
  47. !isLoading &&
  48. traceEvents.length > 0 &&
  49. traceEvents.every(item => item.id === event.id);
  50. if (isError || noEvents || onlySelfEvent || isLoading) {
  51. // display empty placeholder to reduce layout shift
  52. return null;
  53. }
  54. return (
  55. <ErrorBoundary mini>
  56. {timelineStatus === 'shown' ? (
  57. <TimelineWrapper>
  58. <div ref={timelineRef}>
  59. <TimelineEventsContainer>
  60. <TimelineOutline />
  61. {/* Sets a min width of 200 for testing */}
  62. <TraceTimelineEvents event={event} width={Math.max(width, 200)} />
  63. </TimelineEventsContainer>
  64. </div>
  65. <QuestionTooltipWrapper>
  66. <QuestionTooltip
  67. size="sm"
  68. title={t(
  69. 'This is a trace timeline showing all related events happening upstream and downstream of this event'
  70. )}
  71. position="bottom"
  72. />
  73. </QuestionTooltipWrapper>
  74. </TimelineWrapper>
  75. ) : (
  76. <Feature features="related-issues-issue-details-page">
  77. {timelineSkipped && (
  78. // XXX: Temporary. This will need to be replaced with a styled component
  79. <div style={{width: '400px'}}>
  80. {traceEvents.map((traceEvent, index) => (
  81. <div key={index} style={{display: 'flex', alignItems: 'center'}}>
  82. <div
  83. style={{whiteSpace: 'nowrap', minWidth: '75px'}}
  84. data-test-id={`this-event-${traceEvent.id}`}
  85. >
  86. {event.id === traceEvent.id && <span>This event</span>}
  87. </div>
  88. <EventItem key={index} timelineEvent={traceEvent} />
  89. </div>
  90. ))}
  91. </div>
  92. )}
  93. </Feature>
  94. )}
  95. </ErrorBoundary>
  96. );
  97. }
  98. function getIssuesCountFromEvents(events: TimelineEvent[]): number {
  99. const distinctIssues = events.filter(
  100. (event, index, self) =>
  101. event['issue.id'] !== undefined &&
  102. self.findIndex(e => e['issue.id'] === event['issue.id']) === index
  103. );
  104. return distinctIssues.length;
  105. }
  106. const TimelineWrapper = styled('div')`
  107. display: grid;
  108. grid-template-columns: 1fr auto;
  109. align-items: start;
  110. gap: ${space(2)};
  111. margin-top: ${space(0.25)};
  112. `;
  113. const QuestionTooltipWrapper = styled('div')`
  114. margin-top: ${space(0.25)};
  115. `;
  116. /**
  117. * Displays the container the dots appear inside of
  118. */
  119. const TimelineOutline = styled('div')`
  120. position: absolute;
  121. left: 0;
  122. top: 3.5px;
  123. width: 100%;
  124. height: 10px;
  125. border: 1px solid ${p => p.theme.innerBorder};
  126. border-radius: ${p => p.theme.borderRadius};
  127. background-color: ${p => p.theme.backgroundSecondary};
  128. `;
  129. const TimelineEventsContainer = styled('div')`
  130. position: relative;
  131. height: 34px;
  132. padding-top: 10px;
  133. `;