spanOpBreakdown.tsx 5.6 KB

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