eventDisplay.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import {useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {CompactSelect} from 'sentry/components/compactSelect';
  4. import DateTime from 'sentry/components/dateTime';
  5. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  6. import {EventTags} from 'sentry/components/events/eventTags';
  7. import {MINIMAP_HEIGHT} from 'sentry/components/events/interfaces/spans/constants';
  8. import {noFilter} from 'sentry/components/events/interfaces/spans/filter';
  9. import {
  10. ActualMinimap,
  11. MinimapBackground,
  12. } from 'sentry/components/events/interfaces/spans/header';
  13. import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
  14. import OpsBreakdown from 'sentry/components/events/opsBreakdown';
  15. import Link from 'sentry/components/links/link';
  16. import LoadingIndicator from 'sentry/components/loadingIndicator';
  17. import TextOverflow from 'sentry/components/textOverflow';
  18. import {t} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {EventTransaction, Group, Project} from 'sentry/types';
  21. import {defined} from 'sentry/utils';
  22. import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  23. import EventView from 'sentry/utils/discover/eventView';
  24. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  25. import {eventDetailsRoute, generateEventSlug} from 'sentry/utils/discover/urls';
  26. import {getShortEventId} from 'sentry/utils/events';
  27. import {useApiQuery} from 'sentry/utils/queryClient';
  28. import {useLocation} from 'sentry/utils/useLocation';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import {GroupEventActions} from 'sentry/views/issueDetails/groupEventCarousel';
  31. export function getSampleEventQuery({
  32. transaction,
  33. durationBaseline,
  34. addUpperBound = true,
  35. }: {
  36. durationBaseline: number;
  37. transaction: string;
  38. addUpperBound?: boolean;
  39. }) {
  40. const baseQuery = `event.type:transaction transaction:"${transaction}" transaction.duration:>=${
  41. durationBaseline * 0.5
  42. }ms`;
  43. if (addUpperBound) {
  44. return `${baseQuery} transaction.duration:<=${durationBaseline * 1.5}ms`;
  45. }
  46. return baseQuery;
  47. }
  48. // A hook for getting "sample events" for a transaction
  49. // In its current state it will just fetch at most 5 events that match the
  50. // transaction name within a range of the duration baseline provided
  51. function useFetchSampleEvents({
  52. start,
  53. end,
  54. transaction,
  55. durationBaseline,
  56. projectId,
  57. }: {
  58. durationBaseline: number;
  59. end: number;
  60. projectId: number;
  61. start: number;
  62. transaction: string;
  63. }) {
  64. const location = useLocation();
  65. const organization = useOrganization();
  66. const eventView = new EventView({
  67. dataset: DiscoverDatasets.DISCOVER,
  68. // Assumes the start and end timestamps are already in milliseconds
  69. start: new Date(start).toISOString(),
  70. end: new Date(end).toISOString(),
  71. fields: [{field: 'id'}, {field: 'timestamp'}],
  72. query: getSampleEventQuery({transaction, durationBaseline}),
  73. createdBy: undefined,
  74. display: undefined,
  75. id: undefined,
  76. environment: [],
  77. name: undefined,
  78. project: [projectId],
  79. sorts: [],
  80. statsPeriod: undefined,
  81. team: [],
  82. topEvents: undefined,
  83. });
  84. return useDiscoverQuery({
  85. eventView,
  86. location,
  87. orgSlug: organization.slug,
  88. limit: 5,
  89. });
  90. }
  91. type EventDisplayProps = {
  92. durationBaseline: number;
  93. end: number;
  94. eventSelectLabel: string;
  95. group: Group;
  96. project: Project;
  97. start: number;
  98. transaction: string;
  99. };
  100. function EventDisplay({
  101. eventSelectLabel,
  102. project,
  103. start,
  104. end,
  105. transaction,
  106. durationBaseline,
  107. group,
  108. }: EventDisplayProps) {
  109. const location = useLocation();
  110. const organization = useOrganization();
  111. const [selectedEventId, setSelectedEventId] = useState<string>('');
  112. const {data, isLoading, isError} = useFetchSampleEvents({
  113. start,
  114. end,
  115. transaction,
  116. durationBaseline,
  117. projectId: parseInt(project.id, 10),
  118. });
  119. const eventIds = data?.data.map(({id}) => id);
  120. const {data: eventData, isFetching} = useApiQuery<EventTransaction>(
  121. [`/organizations/${organization.slug}/events/${project.slug}:${selectedEventId}/`],
  122. {staleTime: Infinity, retry: false, enabled: !!selectedEventId && !!project.slug}
  123. );
  124. useEffect(() => {
  125. if (defined(eventIds) && eventIds.length > 0 && !selectedEventId) {
  126. setSelectedEventId(eventIds[0]);
  127. }
  128. }, [eventIds, selectedEventId]);
  129. if (isError) {
  130. return null;
  131. }
  132. if (isLoading || isFetching) {
  133. return <LoadingIndicator />;
  134. }
  135. if (!defined(eventData) || !defined(eventIds)) {
  136. return (
  137. <EmptyStateWrapper>
  138. <EmptyStateWarning withIcon>
  139. <div>{t('Unable to find a sample event')}</div>
  140. </EmptyStateWarning>
  141. </EmptyStateWrapper>
  142. );
  143. }
  144. const waterfallModel = new WaterfallModel(eventData);
  145. return (
  146. <EventDisplayContainer>
  147. <div>
  148. <StyledEventSelectorControlBar>
  149. <CompactSelect
  150. size="sm"
  151. disabled={false}
  152. options={eventIds.map(id => ({
  153. value: id,
  154. label: id,
  155. details: <DateTime date={data?.data.find(d => d.id === id)?.timestamp} />,
  156. }))}
  157. value={selectedEventId}
  158. onChange={({value}) => setSelectedEventId(value)}
  159. triggerLabel={
  160. <ButtonLabelWrapper>
  161. <TextOverflow>
  162. {eventSelectLabel}:{' '}
  163. <SelectionTextWrapper>
  164. {getShortEventId(selectedEventId)}
  165. </SelectionTextWrapper>
  166. </TextOverflow>
  167. </ButtonLabelWrapper>
  168. }
  169. />
  170. <GroupEventActions event={eventData} group={group} projectSlug={project.slug} />
  171. </StyledEventSelectorControlBar>
  172. <ComparisonContentWrapper>
  173. <Link
  174. to={eventDetailsRoute({
  175. eventSlug: generateEventSlug({project: project.slug, id: selectedEventId}),
  176. orgSlug: organization.slug,
  177. })}
  178. >
  179. <MinimapContainer>
  180. <MinimapPositioningContainer>
  181. <ActualMinimap
  182. spans={waterfallModel.getWaterfall({
  183. viewStart: 0,
  184. viewEnd: 1,
  185. })}
  186. generateBounds={waterfallModel.generateBounds({
  187. viewStart: 0,
  188. viewEnd: 1,
  189. })}
  190. dividerPosition={0}
  191. rootSpan={waterfallModel.rootSpan.span}
  192. />
  193. </MinimapPositioningContainer>
  194. </MinimapContainer>
  195. </Link>
  196. <OpsBreakdown event={eventData} operationNameFilters={noFilter} hideHeader />
  197. </ComparisonContentWrapper>
  198. </div>
  199. <EventTags
  200. event={eventData}
  201. organization={organization}
  202. projectSlug={project.slug}
  203. location={location}
  204. />
  205. </EventDisplayContainer>
  206. );
  207. }
  208. export {EventDisplay};
  209. const EventDisplayContainer = styled('div')`
  210. display: flex;
  211. gap: ${space(1)};
  212. flex-direction: column;
  213. `;
  214. const ButtonLabelWrapper = styled('span')`
  215. width: 100%;
  216. text-align: left;
  217. align-items: center;
  218. display: inline-grid;
  219. grid-template-columns: 1fr auto;
  220. `;
  221. const StyledEventSelectorControlBar = styled('div')`
  222. display: flex;
  223. align-items: center;
  224. gap: 8px;
  225. margin-bottom: 8px;
  226. `;
  227. const MinimapPositioningContainer = styled('div')`
  228. position: absolute;
  229. top: 0;
  230. width: 100%;
  231. ${MinimapBackground} {
  232. overflow-y: scroll;
  233. }
  234. `;
  235. const MinimapContainer = styled('div')`
  236. height: ${MINIMAP_HEIGHT}px;
  237. max-height: ${MINIMAP_HEIGHT}px;
  238. position: relative;
  239. border-bottom: 1px solid ${p => p.theme.border};
  240. `;
  241. const ComparisonContentWrapper = styled('div')`
  242. border: ${({theme}) => `1px solid ${theme.border}`};
  243. border-radius: ${({theme}) => theme.borderRadius};
  244. overflow: hidden;
  245. `;
  246. const EmptyStateWrapper = styled('div')`
  247. border: ${({theme}) => `1px solid ${theme.border}`};
  248. border-radius: ${({theme}) => theme.borderRadius};
  249. display: flex;
  250. justify-content: center;
  251. align-items: center;
  252. `;
  253. const SelectionTextWrapper = styled('span')`
  254. font-weight: normal;
  255. `;