fullSpanDescription.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import {Fragment, type ReactNode} 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 {IconOpen} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter';
  10. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import {useNavigate} from 'sentry/utils/useNavigate';
  13. import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
  14. import {useFullSpanFromTrace} from 'sentry/views/insights/common/queries/useFullSpanFromTrace';
  15. import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
  16. import {prettyPrintJsonString} from 'sentry/views/insights/database/utils/jsonUtils';
  17. import {ModuleName, SpanIndexedField} from 'sentry/views/insights/types';
  18. const formatter = new SQLishFormatter();
  19. const INDEXED_SPAN_SORT = {
  20. field: 'span.self_time',
  21. kind: 'desc' as const,
  22. };
  23. interface Props {
  24. moduleName: ModuleName;
  25. filters?: Record<string, string>;
  26. group?: string;
  27. shortDescription?: string;
  28. }
  29. export function FullSpanDescription({
  30. group,
  31. shortDescription,
  32. filters,
  33. moduleName,
  34. }: Props) {
  35. const {data: indexedSpans, isFetching: areIndexedSpansLoading} = useSpansIndexed(
  36. {
  37. search: MutableSearch.fromQueryObject({'span.group': group}),
  38. limit: 1,
  39. fields: [
  40. SpanIndexedField.PROJECT_ID,
  41. SpanIndexedField.TRANSACTION_ID,
  42. SpanIndexedField.SPAN_DESCRIPTION,
  43. ],
  44. },
  45. 'api.starfish.span-description'
  46. );
  47. const indexedSpan = indexedSpans?.[0];
  48. // This is used as backup in case we don't have the necessary data available in the indexed span
  49. const {
  50. data: fullSpan,
  51. isLoading,
  52. isFetching,
  53. } = useFullSpanFromTrace(group, [INDEXED_SPAN_SORT], Boolean(indexedSpan), filters);
  54. const description =
  55. indexedSpan?.['span.description'] ?? fullSpan?.description ?? shortDescription;
  56. const system = fullSpan?.data?.['db.system'];
  57. if (areIndexedSpansLoading || (isLoading && isFetching)) {
  58. return (
  59. <PaddedSpinner>
  60. <LoadingIndicator mini hideMessage relative />
  61. </PaddedSpinner>
  62. );
  63. }
  64. if (!description) {
  65. return null;
  66. }
  67. if (moduleName === ModuleName.DB) {
  68. if (system === 'mongodb') {
  69. let stringifiedQuery = '';
  70. let result: ReturnType<typeof prettyPrintJsonString> | undefined = undefined;
  71. if (indexedSpan?.['span.description']) {
  72. result = prettyPrintJsonString(indexedSpan?.['span.description']);
  73. } else if (description) {
  74. result = prettyPrintJsonString(description);
  75. } else if (fullSpan?.sentry_tags?.description) {
  76. result = prettyPrintJsonString(fullSpan?.sentry_tags.description);
  77. } else if (fullSpan?.sentry_tags?.description) {
  78. result = prettyPrintJsonString(fullSpan?.sentry_tags?.description);
  79. } else {
  80. stringifiedQuery = description || fullSpan?.sentry_tags?.description || 'N/A';
  81. }
  82. if (result) {
  83. stringifiedQuery = result.prettifiedQuery;
  84. }
  85. return (
  86. <QueryClippedBox group={group}>
  87. <CodeSnippet language="json">{stringifiedQuery}</CodeSnippet>
  88. </QueryClippedBox>
  89. );
  90. }
  91. return (
  92. <QueryClippedBox group={group}>
  93. <CodeSnippet language="sql">
  94. {formatter.toString(description, {maxLineLength: LINE_LENGTH})}
  95. </CodeSnippet>
  96. </QueryClippedBox>
  97. );
  98. }
  99. if (moduleName === ModuleName.RESOURCE) {
  100. return <CodeSnippet language="http">{description}</CodeSnippet>;
  101. }
  102. return <Fragment>{description}</Fragment>;
  103. }
  104. type TruncatedQueryClipBoxProps = {
  105. children: ReactNode;
  106. group: string | undefined;
  107. };
  108. function QueryClippedBox({group, children}: TruncatedQueryClipBoxProps) {
  109. const navigate = useNavigate();
  110. const databaseURL = useModuleURL(ModuleName.DB);
  111. const location = useLocation();
  112. return (
  113. <StyledClippedBox
  114. btnText={t('View full query')}
  115. clipHeight={500}
  116. buttonProps={{
  117. icon: <IconOpen />,
  118. onClick: () =>
  119. navigate({
  120. pathname: `${databaseURL}/spans/span/${group}`,
  121. query: {...location.query, isExpanded: true},
  122. }),
  123. }}
  124. >
  125. {children}
  126. </StyledClippedBox>
  127. );
  128. }
  129. const LINE_LENGTH = 60;
  130. const PaddedSpinner = styled('div')`
  131. padding: 0 ${space(0.5)};
  132. `;
  133. const StyledClippedBox = styled(ClippedBox)`
  134. > div > div {
  135. z-index: 1;
  136. }
  137. `;