aggregateSpans.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import Alert from 'sentry/components/alert';
  5. import TraceView from 'sentry/components/events/interfaces/spans/traceView';
  6. import {AggregateSpanType} from 'sentry/components/events/interfaces/spans/types';
  7. import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  10. import Panel from 'sentry/components/panels/panel';
  11. import {IconClose} from 'sentry/icons';
  12. import {tct} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {AggregateEventTransaction, EntryType, EventOrGroupType} from 'sentry/types/event';
  15. import {defined} from 'sentry/utils';
  16. import {useApiQuery} from 'sentry/utils/queryClient';
  17. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. type AggregateSpanRow = {
  21. 'avg(absolute_offset)': number;
  22. 'avg(duration)': number;
  23. 'avg(exclusive_time)': number;
  24. 'count()': number;
  25. description: string;
  26. group: string;
  27. is_segment: number;
  28. node_fingerprint: string;
  29. parent_node_fingerprint: string;
  30. start_ms: number;
  31. };
  32. export function useAggregateSpans({transaction}) {
  33. const organization = useOrganization();
  34. const {selection} = usePageFilters();
  35. const endpointOptions = {
  36. query: {
  37. transaction,
  38. project: selection.projects,
  39. environment: selection.environments,
  40. ...normalizeDateTimeParams(selection.datetime),
  41. },
  42. };
  43. return useApiQuery<{
  44. data: {[fingerprint: string]: AggregateSpanRow}[];
  45. meta: any;
  46. }>(
  47. [
  48. `/organizations/${organization.slug}/spans-aggregation/`,
  49. {
  50. query: endpointOptions.query,
  51. },
  52. ],
  53. {
  54. staleTime: Infinity,
  55. enabled: true,
  56. }
  57. );
  58. }
  59. type Props = {
  60. transaction: string;
  61. };
  62. export function AggregateSpans({transaction}: Props) {
  63. const organization = useOrganization();
  64. const {data, isLoading} = useAggregateSpans({transaction});
  65. const [isBannerOpen, setIsBannerOpen] = useLocalStorageState<boolean>(
  66. 'aggregate-waterfall-info-banner',
  67. true
  68. );
  69. function formatSpan(span, total) {
  70. const {
  71. node_fingerprint: span_id,
  72. parent_node_fingerprint: parent_span_id,
  73. description: description,
  74. 'avg(exclusive_time)': exclusive_time,
  75. 'avg(absolute_offset)': start_timestamp,
  76. 'count()': count,
  77. 'avg(duration)': duration,
  78. ...rest
  79. } = span;
  80. return {
  81. ...rest,
  82. span_id,
  83. parent_span_id,
  84. description,
  85. exclusive_time,
  86. timestamp: (start_timestamp + duration) / 1000,
  87. start_timestamp: start_timestamp / 1000,
  88. trace_id: '1', // not actually trace_id just a placeholder
  89. count,
  90. total,
  91. duration,
  92. frequency: count / total,
  93. type: 'aggregate',
  94. };
  95. }
  96. const totalCount: number = useMemo(() => {
  97. if (defined(data)) {
  98. const spans = Object.values(data);
  99. for (let index = 0; index < spans.length; index++) {
  100. if (spans[index].is_segment) {
  101. return spans[index]['count()'];
  102. }
  103. }
  104. }
  105. return 0;
  106. }, [data]);
  107. const spanList: AggregateSpanType[] = useMemo(() => {
  108. const spanList_: AggregateSpanType[] = [];
  109. if (defined(data)) {
  110. const spans = Object.values(data);
  111. for (let index = 0; index < spans.length; index++) {
  112. const node = formatSpan(spans[index], totalCount);
  113. if (node.is_segment === 1) {
  114. spanList_.unshift(node);
  115. } else {
  116. spanList_.push(node);
  117. }
  118. }
  119. }
  120. return spanList_;
  121. }, [data, totalCount]);
  122. const [parentSpan, ...flattenedSpans] = spanList;
  123. const event: AggregateEventTransaction = useMemo(() => {
  124. return {
  125. contexts: {
  126. trace: {
  127. ...omit(parentSpan, 'type'),
  128. },
  129. },
  130. endTimestamp: 0,
  131. entries: [
  132. {
  133. data: flattenedSpans,
  134. type: EntryType.SPANS,
  135. },
  136. ],
  137. startTimestamp: 0,
  138. type: EventOrGroupType.AGGREGATE_TRANSACTION,
  139. // TODO: No need for optional chaining here, we should not return anything if the event is not loaded
  140. frequency: parentSpan?.frequency ?? 0,
  141. count: parentSpan?.count ?? 0,
  142. total: parentSpan?.total ?? 0,
  143. };
  144. }, [parentSpan, flattenedSpans]);
  145. const waterfallModel = useMemo(() => new WaterfallModel(event, undefined), [event]);
  146. if (isLoading) {
  147. return <LoadingIndicator />;
  148. }
  149. return (
  150. <Fragment>
  151. {isBannerOpen && (
  152. <StyledAlert
  153. type="info"
  154. showIcon
  155. trailingItems={<StyledCloseButton onClick={() => setIsBannerOpen(false)} />}
  156. >
  157. {tct(
  158. 'This is an aggregate view across [x] events. You can see how frequent each span appears in the aggregate and identify any outliers.',
  159. {x: event.count}
  160. )}
  161. </StyledAlert>
  162. )}
  163. <Panel>
  164. <TraceView
  165. waterfallModel={waterfallModel}
  166. organization={organization}
  167. isEmbedded
  168. isAggregate
  169. />
  170. </Panel>
  171. </Fragment>
  172. );
  173. }
  174. const StyledAlert = styled(Alert)`
  175. margin-bottom: ${space(2)};
  176. `;
  177. const StyledCloseButton = styled(IconClose)`
  178. cursor: pointer;
  179. `;