fullSpanDescription.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  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 {
  78. stringifiedQuery = description || fullSpan?.sentry_tags?.description || 'N/A';
  79. }
  80. if (result) {
  81. stringifiedQuery = result.prettifiedQuery;
  82. }
  83. return (
  84. <QueryClippedBox group={group}>
  85. <CodeSnippet language="json">{stringifiedQuery}</CodeSnippet>
  86. </QueryClippedBox>
  87. );
  88. }
  89. return (
  90. <QueryClippedBox group={group}>
  91. <CodeSnippet language="sql">
  92. {formatter.toString(description, {maxLineLength: LINE_LENGTH})}
  93. </CodeSnippet>
  94. </QueryClippedBox>
  95. );
  96. }
  97. if (moduleName === ModuleName.RESOURCE) {
  98. return <CodeSnippet language="http">{description}</CodeSnippet>;
  99. }
  100. return <Fragment>{description}</Fragment>;
  101. }
  102. type TruncatedQueryClipBoxProps = {
  103. children: ReactNode;
  104. group: string | undefined;
  105. };
  106. function QueryClippedBox({group, children}: TruncatedQueryClipBoxProps) {
  107. const navigate = useNavigate();
  108. const databaseURL = useModuleURL(ModuleName.DB);
  109. const location = useLocation();
  110. return (
  111. <StyledClippedBox
  112. btnText={t('View full query')}
  113. clipHeight={500}
  114. buttonProps={{
  115. icon: <IconOpen />,
  116. onClick: () =>
  117. navigate({
  118. pathname: `${databaseURL}/spans/span/${group}`,
  119. query: {...location.query, isExpanded: true},
  120. }),
  121. }}
  122. >
  123. {children}
  124. </StyledClippedBox>
  125. );
  126. }
  127. const LINE_LENGTH = 60;
  128. const PaddedSpinner = styled('div')`
  129. padding: 0 ${space(0.5)};
  130. `;
  131. const StyledClippedBox = styled(ClippedBox)`
  132. > div > div {
  133. z-index: 1;
  134. }
  135. `;