index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {PureComponent} from 'react';
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Observer} from 'mobx-react';
  5. import Alert from 'sentry/components/alert';
  6. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  7. import List from 'sentry/components/list';
  8. import ListItem from 'sentry/components/list/listItem';
  9. import {Panel} from 'sentry/components/panels';
  10. import SearchBar from 'sentry/components/searchBar';
  11. import {IconWarning} from 'sentry/icons';
  12. import {t, tct, tn} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. import {Organization} from 'sentry/types';
  15. import {EventTransaction} from 'sentry/types/event';
  16. import {objectIsEmpty} from 'sentry/utils';
  17. import * as QuickTraceContext from 'sentry/utils/performance/quickTrace/quickTraceContext';
  18. import {TraceError} from 'sentry/utils/performance/quickTrace/types';
  19. import withOrganization from 'sentry/utils/withOrganization';
  20. import * as AnchorLinkManager from './anchorLinkManager';
  21. import Filter from './filter';
  22. import TraceView from './traceView';
  23. import {ParsedTraceType} from './types';
  24. import {parseTrace, scrollToSpan} from './utils';
  25. import WaterfallModel from './waterfallModel';
  26. type Props = {
  27. event: EventTransaction;
  28. organization: Organization;
  29. } & WithRouterProps;
  30. type State = {
  31. parsedTrace: ParsedTraceType;
  32. waterfallModel: WaterfallModel;
  33. };
  34. class SpansInterface extends PureComponent<Props, State> {
  35. state: State = {
  36. parsedTrace: parseTrace(this.props.event),
  37. waterfallModel: new WaterfallModel(this.props.event),
  38. };
  39. static getDerivedStateFromProps(props: Readonly<Props>, state: State): State {
  40. if (state.waterfallModel.isEvent(props.event)) {
  41. return state;
  42. }
  43. return {
  44. ...state,
  45. parsedTrace: parseTrace(props.event),
  46. waterfallModel: new WaterfallModel(props.event),
  47. };
  48. }
  49. handleSpanFilter = (searchQuery: string) => {
  50. const {waterfallModel} = this.state;
  51. waterfallModel.querySpanSearch(searchQuery);
  52. };
  53. renderTraceErrorsAlert({
  54. isLoading,
  55. errors,
  56. parsedTrace,
  57. }: {
  58. errors: TraceError[] | undefined;
  59. isLoading: boolean;
  60. parsedTrace: ParsedTraceType;
  61. }) {
  62. if (isLoading) {
  63. return null;
  64. }
  65. if (!errors || errors.length <= 0) {
  66. return null;
  67. }
  68. // This is intentional as unbalanced string formatters in `tn()` are problematic
  69. const label =
  70. errors.length === 1
  71. ? t('There is an error event associated with this transaction event.')
  72. : tn(
  73. `There are %s error events associated with this transaction event.`,
  74. `There are %s error events associated with this transaction event.`,
  75. errors.length
  76. );
  77. // mapping from span ids to the span op and the number of errors in that span
  78. const errorsMap: {
  79. [spanId: string]: {errorsCount: number; operation: string};
  80. } = {};
  81. errors.forEach(error => {
  82. if (!errorsMap[error.span]) {
  83. // first check of the error belongs to the root span
  84. if (parsedTrace.rootSpanID === error.span) {
  85. errorsMap[error.span] = {
  86. operation: parsedTrace.op,
  87. errorsCount: 0,
  88. };
  89. } else {
  90. // since it does not belong to the root span, check if it belongs
  91. // to one of the other spans in the transaction
  92. const span = parsedTrace.spans.find(s => s.span_id === error.span);
  93. if (!span?.op) {
  94. return;
  95. }
  96. errorsMap[error.span] = {
  97. operation: span.op,
  98. errorsCount: 0,
  99. };
  100. }
  101. }
  102. errorsMap[error.span].errorsCount++;
  103. });
  104. return (
  105. <AlertContainer>
  106. <Alert type="error" icon={<IconWarning size="md" />}>
  107. <ErrorLabel>{label}</ErrorLabel>
  108. <AnchorLinkManager.Consumer>
  109. {({scrollToHash}) => (
  110. <List symbol="bullet">
  111. {Object.entries(errorsMap).map(([spanId, {operation, errorsCount}]) => (
  112. <ListItem key={spanId}>
  113. {tct('[errors] [link]', {
  114. errors: tn('%s error in ', '%s errors in ', errorsCount),
  115. link: (
  116. <ErrorLink
  117. onClick={scrollToSpan(
  118. spanId,
  119. scrollToHash,
  120. this.props.location
  121. )}
  122. >
  123. {operation}
  124. </ErrorLink>
  125. ),
  126. })}
  127. </ListItem>
  128. ))}
  129. </List>
  130. )}
  131. </AnchorLinkManager.Consumer>
  132. </Alert>
  133. </AlertContainer>
  134. );
  135. }
  136. render() {
  137. const {event, organization} = this.props;
  138. const {parsedTrace, waterfallModel} = this.state;
  139. return (
  140. <Container hasErrors={!objectIsEmpty(event.errors)}>
  141. <QuickTraceContext.Consumer>
  142. {quickTrace => (
  143. <AnchorLinkManager.Provider>
  144. {this.renderTraceErrorsAlert({
  145. isLoading: quickTrace?.isLoading || false,
  146. errors: quickTrace?.currentEvent?.errors,
  147. parsedTrace,
  148. })}
  149. <Observer>
  150. {() => {
  151. return (
  152. <Search>
  153. <Filter
  154. operationNameCounts={waterfallModel.operationNameCounts}
  155. operationNameFilter={waterfallModel.operationNameFilters}
  156. toggleOperationNameFilter={
  157. waterfallModel.toggleOperationNameFilter
  158. }
  159. toggleAllOperationNameFilters={
  160. waterfallModel.toggleAllOperationNameFilters
  161. }
  162. />
  163. <StyledSearchBar
  164. defaultQuery=""
  165. query={waterfallModel.searchQuery || ''}
  166. placeholder={t('Search for spans')}
  167. onSearch={this.handleSpanFilter}
  168. />
  169. </Search>
  170. );
  171. }}
  172. </Observer>
  173. <Panel>
  174. <Observer>
  175. {() => {
  176. return (
  177. <TraceView
  178. waterfallModel={waterfallModel}
  179. organization={organization}
  180. />
  181. );
  182. }}
  183. </Observer>
  184. <GuideAnchorWrapper>
  185. <GuideAnchor target="span_tree" position="bottom" />
  186. </GuideAnchorWrapper>
  187. </Panel>
  188. </AnchorLinkManager.Provider>
  189. )}
  190. </QuickTraceContext.Consumer>
  191. </Container>
  192. );
  193. }
  194. }
  195. const GuideAnchorWrapper = styled('div')`
  196. height: 0;
  197. width: 0;
  198. margin-left: 50%;
  199. `;
  200. const Container = styled('div')<{hasErrors: boolean}>`
  201. ${p =>
  202. p.hasErrors &&
  203. `
  204. padding: ${space(2)} 0;
  205. @media (min-width: ${p.theme.breakpoints[0]}) {
  206. padding: ${space(3)} 0 0 0;
  207. }
  208. `}
  209. `;
  210. const ErrorLink = styled('a')`
  211. color: ${p => p.theme.textColor};
  212. :hover {
  213. color: ${p => p.theme.textColor};
  214. }
  215. `;
  216. const Search = styled('div')`
  217. display: flex;
  218. width: 100%;
  219. margin-bottom: ${space(1)};
  220. `;
  221. const StyledSearchBar = styled(SearchBar)`
  222. flex-grow: 1;
  223. `;
  224. const AlertContainer = styled('div')`
  225. margin-bottom: ${space(1)};
  226. `;
  227. const ErrorLabel = styled('div')`
  228. margin-bottom: ${space(1)};
  229. `;
  230. export default withRouter(withOrganization(SpansInterface));