spanDescription.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import ClippedBox from 'sentry/components/clippedBox';
  4. import {CodeSnippet} from 'sentry/components/codeSnippet';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {space} from 'sentry/styles/space';
  7. import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter';
  8. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  9. import {useLocation} from 'sentry/utils/useLocation';
  10. import {useNavigate} from 'sentry/utils/useNavigate';
  11. import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
  12. import {useFullSpanFromTrace} from 'sentry/views/insights/common/queries/useFullSpanFromTrace';
  13. import {
  14. MissingFrame,
  15. StackTraceMiniFrame,
  16. } from 'sentry/views/insights/database/components/stackTraceMiniFrame';
  17. import {SupportedDatabaseSystem} from 'sentry/views/insights/database/utils/constants';
  18. import {
  19. isValidJson,
  20. prettyPrintJsonString,
  21. } from 'sentry/views/insights/database/utils/jsonUtils';
  22. import type {SpanIndexedFieldTypes} from 'sentry/views/insights/types';
  23. import {SpanIndexedField} from 'sentry/views/insights/types';
  24. interface Props {
  25. groupId: SpanIndexedFieldTypes[SpanIndexedField.SPAN_GROUP];
  26. op: SpanIndexedFieldTypes[SpanIndexedField.SPAN_OP];
  27. preliminaryDescription?: string;
  28. }
  29. export function SpanDescription(props: Props) {
  30. const {op, preliminaryDescription} = props;
  31. if (op.startsWith('db')) {
  32. return <DatabaseSpanDescription {...props} />;
  33. }
  34. return <WordBreak>{preliminaryDescription ?? ''}</WordBreak>;
  35. }
  36. const formatter = new SQLishFormatter();
  37. export function DatabaseSpanDescription({
  38. groupId,
  39. preliminaryDescription,
  40. }: Omit<Props, 'op'>) {
  41. const navigate = useNavigate();
  42. const location = useLocation();
  43. const {data: indexedSpans, isFetching: areIndexedSpansLoading} = useSpansIndexed(
  44. {
  45. search: MutableSearch.fromQueryObject({'span.group': groupId}),
  46. limit: 1,
  47. fields: [
  48. SpanIndexedField.PROJECT_ID,
  49. SpanIndexedField.TRANSACTION_ID,
  50. SpanIndexedField.SPAN_DESCRIPTION,
  51. ],
  52. },
  53. 'api.starfish.span-description'
  54. );
  55. const indexedSpan = indexedSpans?.[0];
  56. // NOTE: We only need this for `span.data`! If this info existed in indexed spans, we could skip it
  57. const {data: rawSpan, isFetching: isRawSpanLoading} = useFullSpanFromTrace(
  58. groupId,
  59. [INDEXED_SPAN_SORT],
  60. Boolean(indexedSpan)
  61. );
  62. // isExpanded is a query param that is meant to be accessed only when clicking on the
  63. // "View full query" button from the hover tooltip. It is removed from the query params
  64. // on the initial load so the value is not persisted through the link
  65. const [isExpanded] = useState<boolean>(() => Boolean(location.query.isExpanded));
  66. useEffect(() => {
  67. navigate(
  68. {...location, query: {...location.query, isExpanded: undefined}},
  69. {replace: true}
  70. );
  71. // Skip the `location` dependency because it will cause this effect to trigger infinitely, since
  72. // `navigate` will update the location within this effect
  73. // eslint-disable-next-line react-hooks/exhaustive-deps
  74. }, [navigate]);
  75. const system = rawSpan?.data?.['db.system'];
  76. const formattedDescription = useMemo(() => {
  77. const rawDescription =
  78. rawSpan?.description || indexedSpan?.['span.description'] || preliminaryDescription;
  79. if (system === SupportedDatabaseSystem.MONGODB) {
  80. let bestDescription = '';
  81. if (
  82. rawSpan?.sentry_tags?.description &&
  83. isValidJson(rawSpan.sentry_tags.description)
  84. ) {
  85. bestDescription = rawSpan.sentry_tags.description;
  86. } else if (preliminaryDescription && isValidJson(preliminaryDescription)) {
  87. bestDescription = preliminaryDescription;
  88. } else if (
  89. indexedSpan?.['span.description'] &&
  90. isValidJson(indexedSpan?.['span.description'])
  91. ) {
  92. bestDescription = indexedSpan?.['span.description'];
  93. } else if (rawSpan?.description && isValidJson(rawSpan.description)) {
  94. bestDescription = rawSpan?.description;
  95. } else {
  96. return rawDescription ?? 'N/A';
  97. }
  98. return prettyPrintJsonString(bestDescription).prettifiedQuery;
  99. }
  100. return formatter.toString(rawDescription ?? '');
  101. }, [preliminaryDescription, rawSpan, indexedSpan, system]);
  102. return (
  103. <Frame>
  104. {areIndexedSpansLoading || isRawSpanLoading ? (
  105. <WithPadding>
  106. <LoadingIndicator mini />
  107. </WithPadding>
  108. ) : (
  109. <QueryClippedBox clipHeight={500} isExpanded={isExpanded}>
  110. <CodeSnippet language={system === 'mongodb' ? 'json' : 'sql'} isRounded={false}>
  111. {formattedDescription ?? ''}
  112. </CodeSnippet>
  113. </QueryClippedBox>
  114. )}
  115. {!areIndexedSpansLoading && !isRawSpanLoading && (
  116. <Fragment>
  117. {rawSpan?.data?.['code.filepath'] ? (
  118. <StackTraceMiniFrame
  119. projectId={indexedSpan?.project_id?.toString()}
  120. eventId={indexedSpan?.['transaction.id']}
  121. frame={{
  122. filename: rawSpan?.data?.['code.filepath'],
  123. lineNo: rawSpan?.data?.['code.lineno'],
  124. function: rawSpan?.data?.['code.function'],
  125. }}
  126. />
  127. ) : (
  128. <MissingFrame system={system} />
  129. )}
  130. </Fragment>
  131. )}
  132. </Frame>
  133. );
  134. }
  135. function QueryClippedBox(props) {
  136. const {isExpanded, children} = props;
  137. if (isExpanded) {
  138. return children;
  139. }
  140. return <StyledClippedBox {...props} />;
  141. }
  142. const INDEXED_SPAN_SORT = {
  143. field: 'span.self_time',
  144. kind: 'desc' as const,
  145. };
  146. export const Frame = styled('div')`
  147. border: solid 1px ${p => p.theme.border};
  148. border-radius: ${p => p.theme.borderRadius};
  149. overflow: hidden;
  150. `;
  151. const WithPadding = styled('div')`
  152. display: flex;
  153. padding: ${space(1)} ${space(2)};
  154. `;
  155. const WordBreak = styled('div')`
  156. word-break: break-word;
  157. `;
  158. const StyledClippedBox = styled(ClippedBox)`
  159. padding: 0;
  160. > div > div {
  161. z-index: 1;
  162. }
  163. `;