spanOpBreakdown.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Location} from 'history';
  4. import moment from 'moment';
  5. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  6. import {DataSection} from 'sentry/components/events/styles';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {Event} from 'sentry/types';
  11. import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
  12. import EventView from 'sentry/utils/discover/eventView';
  13. import {useLocation} from 'sentry/utils/useLocation';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import PieChart from './pieChart';
  16. const SPAN_OPS = ['db', 'http', 'resource', 'browser', 'ui'];
  17. const REQUEST_FIELDS = SPAN_OPS.map(op => ({field: `p100(spans.${op})`}));
  18. const SPAN_OPS_NAME_MAP = {
  19. ['p100(spans.db)']: 'db',
  20. ['p100(spans.http)']: 'http',
  21. ['p100(spans.resource)']: 'resource',
  22. ['p100(spans.browser)']: 'browser',
  23. ['p100(spans.ui)']: 'ui',
  24. };
  25. function getPostBreakpointEventView(location: Location, event: Event, end: number) {
  26. const eventView = EventView.fromLocation(location);
  27. eventView.fields = REQUEST_FIELDS;
  28. if (event?.occurrence) {
  29. const {breakpoint, aggregateRange2, transaction} = event?.occurrence?.evidenceData;
  30. eventView.start = new Date(breakpoint * 1000).toISOString();
  31. eventView.end = new Date(end).toISOString();
  32. eventView.query = `event.type:transaction transaction:"${transaction}" transaction.duration:<${
  33. aggregateRange2 * 1.15
  34. }`;
  35. }
  36. return eventView;
  37. }
  38. function getPreBreakpointEventView(location: Location, event: Event) {
  39. const retentionPeriodMs = moment().subtract(90, 'days').valueOf();
  40. const eventView = EventView.fromLocation(location);
  41. eventView.fields = REQUEST_FIELDS;
  42. if (event?.occurrence) {
  43. const {breakpoint, aggregateRange1, transaction, dataStart} =
  44. event?.occurrence?.evidenceData;
  45. eventView.start = new Date(
  46. Math.max(dataStart * 1000, retentionPeriodMs)
  47. ).toISOString();
  48. eventView.end = new Date(breakpoint * 1000).toISOString();
  49. eventView.query = `event.type:transaction transaction:"${transaction}" transaction.duration:<${
  50. aggregateRange1 * 1.15
  51. }`;
  52. }
  53. return eventView;
  54. }
  55. function EventSpanOpBreakdown({event}: {event: Event}) {
  56. const organization = useOrganization();
  57. const location = useLocation();
  58. const now = useMemo(() => Date.now(), []);
  59. const postBreakpointEventView = getPostBreakpointEventView(location, event, now);
  60. const preBreakpointEventView = getPreBreakpointEventView(location, event);
  61. const queryExtras = {dataset: 'metricsEnhanced'};
  62. const {
  63. data: postBreakpointData,
  64. isLoading: postBreakpointIsLoading,
  65. isError: postBreakpointIsError,
  66. } = useDiscoverQuery({
  67. eventView: postBreakpointEventView,
  68. orgSlug: organization.slug,
  69. location,
  70. queryExtras,
  71. });
  72. const {
  73. data: preBreakpointData,
  74. isLoading: preBreakpointIsLoading,
  75. isError: preBreakpointIsError,
  76. } = useDiscoverQuery({
  77. eventView: preBreakpointEventView,
  78. orgSlug: organization.slug,
  79. location,
  80. queryExtras,
  81. });
  82. const postBreakpointPrunedSpanOps = Object.entries(postBreakpointData?.data[0] || {})
  83. .filter(entry => (entry[1] as number) > 0)
  84. .map(entry => ({
  85. value: entry[1] as number,
  86. name: SPAN_OPS_NAME_MAP[entry[0]],
  87. }));
  88. const spanOpDiffs = SPAN_OPS.map(op => {
  89. const preBreakpointValue =
  90. (preBreakpointData?.data[0][`p100(spans.${op})`] as string) || undefined;
  91. const preBreakpointValueAsNumber = preBreakpointValue
  92. ? parseInt(preBreakpointValue, 10)
  93. : 0;
  94. const postBreakpointValue =
  95. (postBreakpointData?.data[0][`p100(spans.${op})`] as string) || undefined;
  96. const postBreakpointValueAsNumber = postBreakpointValue
  97. ? parseInt(postBreakpointValue, 10)
  98. : 0;
  99. if (preBreakpointValueAsNumber === 0 || postBreakpointValueAsNumber === 0) {
  100. return null;
  101. }
  102. return {
  103. [op]: {
  104. percentChange: postBreakpointValueAsNumber / preBreakpointValueAsNumber,
  105. oldBaseline: preBreakpointValueAsNumber,
  106. newBaseline: postBreakpointValueAsNumber,
  107. },
  108. };
  109. })
  110. .filter(Boolean)
  111. .reduce((acc, opDiffData) => {
  112. if (opDiffData && acc) {
  113. Object.keys(opDiffData).forEach(op => {
  114. acc[op] = opDiffData[op];
  115. });
  116. }
  117. return acc;
  118. }, {});
  119. const series = [
  120. {
  121. seriesName: 'Aggregate Span Op Breakdown',
  122. data: postBreakpointPrunedSpanOps,
  123. },
  124. ];
  125. if (postBreakpointIsLoading || preBreakpointIsLoading) {
  126. return (
  127. <Wrapper>
  128. <LoadingIndicator />
  129. </Wrapper>
  130. );
  131. }
  132. if (postBreakpointIsError || preBreakpointIsError) {
  133. return (
  134. <Wrapper>
  135. <EmptyStateWrapper>
  136. <EmptyStateWarning withIcon>
  137. <div>{t('Unable to fetch span breakdowns')}</div>
  138. </EmptyStateWarning>
  139. </EmptyStateWrapper>
  140. </Wrapper>
  141. );
  142. }
  143. return (
  144. <Wrapper>
  145. <DataSection>
  146. <strong>{t('Operation Breakdown:')}</strong>
  147. <PieChart data={spanOpDiffs} series={series} />
  148. </DataSection>
  149. </Wrapper>
  150. );
  151. }
  152. const EmptyStateWrapper = styled('div')`
  153. border: ${({theme}) => `1px solid ${theme.border}`};
  154. border-radius: ${({theme}) => theme.borderRadius};
  155. display: flex;
  156. justify-content: center;
  157. align-items: center;
  158. margin: ${space(1.5)} ${space(4)};
  159. `;
  160. const Wrapper = styled('div')`
  161. display: grid;
  162. grid-template-columns: 1fr 1fr;
  163. `;
  164. export default EventSpanOpBreakdown;