import {Fragment, useEffect, useMemo, useState} from 'react';
import styled from '@emotion/styled';
import ClippedBox from 'sentry/components/clippedBox';
import {CodeSnippet} from 'sentry/components/codeSnippet';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {space} from 'sentry/styles/space';
import {SQLishFormatter} from 'sentry/utils/sqlish/SQLishFormatter';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
import {useFullSpanFromTrace} from 'sentry/views/insights/common/queries/useFullSpanFromTrace';
import {
MissingFrame,
StackTraceMiniFrame,
} from 'sentry/views/insights/database/components/stackTraceMiniFrame';
import {SupportedDatabaseSystem} from 'sentry/views/insights/database/utils/constants';
import {
isValidJson,
prettyPrintJsonString,
} from 'sentry/views/insights/database/utils/jsonUtils';
import type {SpanIndexedFieldTypes} from 'sentry/views/insights/types';
import {SpanIndexedField} from 'sentry/views/insights/types';
interface Props {
groupId: SpanIndexedFieldTypes[SpanIndexedField.SPAN_GROUP];
op: SpanIndexedFieldTypes[SpanIndexedField.SPAN_OP];
preliminaryDescription?: string;
}
export function SpanDescription(props: Props) {
const {op, preliminaryDescription} = props;
if (op.startsWith('db')) {
return ;
}
return {preliminaryDescription ?? ''};
}
const formatter = new SQLishFormatter();
export function DatabaseSpanDescription({
groupId,
preliminaryDescription,
}: Omit) {
const navigate = useNavigate();
const location = useLocation();
const {data: indexedSpans, isFetching: areIndexedSpansLoading} = useSpansIndexed(
{
search: MutableSearch.fromQueryObject({'span.group': groupId}),
limit: 1,
fields: [
SpanIndexedField.PROJECT_ID,
SpanIndexedField.TRANSACTION_ID,
SpanIndexedField.SPAN_DESCRIPTION,
],
},
'api.starfish.span-description'
);
const indexedSpan = indexedSpans?.[0];
// NOTE: We only need this for `span.data`! If this info existed in indexed spans, we could skip it
const {data: rawSpan, isFetching: isRawSpanLoading} = useFullSpanFromTrace(
groupId,
[INDEXED_SPAN_SORT],
Boolean(indexedSpan)
);
// isExpanded is a query param that is meant to be accessed only when clicking on the
// "View full query" button from the hover tooltip. It is removed from the query params
// on the initial load so the value is not persisted through the link
const [isExpanded] = useState(() => Boolean(location.query.isExpanded));
useEffect(() => {
navigate(
{...location, query: {...location.query, isExpanded: undefined}},
{replace: true}
);
// Skip the `location` dependency because it will cause this effect to trigger infinitely, since
// `navigate` will update the location within this effect
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigate]);
const system = rawSpan?.data?.['db.system'];
const formattedDescription = useMemo(() => {
const rawDescription =
rawSpan?.description || indexedSpan?.['span.description'] || preliminaryDescription;
if (system === SupportedDatabaseSystem.MONGODB) {
let bestDescription = '';
if (
rawSpan?.sentry_tags?.description &&
isValidJson(rawSpan.sentry_tags.description)
) {
bestDescription = rawSpan.sentry_tags.description;
} else if (preliminaryDescription && isValidJson(preliminaryDescription)) {
bestDescription = preliminaryDescription;
} else if (
indexedSpan?.['span.description'] &&
isValidJson(indexedSpan?.['span.description'])
) {
bestDescription = indexedSpan?.['span.description'];
} else if (rawSpan?.description && isValidJson(rawSpan.description)) {
bestDescription = rawSpan?.description;
} else {
return rawDescription ?? 'N/A';
}
return prettyPrintJsonString(bestDescription).prettifiedQuery;
}
return formatter.toString(rawDescription ?? '');
}, [preliminaryDescription, rawSpan, indexedSpan, system]);
return (
{areIndexedSpansLoading || isRawSpanLoading ? (
) : (
{formattedDescription ?? ''}
)}
{!areIndexedSpansLoading && !isRawSpanLoading && (
{rawSpan?.data?.['code.filepath'] ? (
) : (
)}
)}
);
}
function QueryClippedBox(props) {
const {isExpanded, children} = props;
if (isExpanded) {
return children;
}
return ;
}
const INDEXED_SPAN_SORT = {
field: 'span.self_time',
kind: 'desc' as const,
};
export const Frame = styled('div')`
border: solid 1px ${p => p.theme.border};
border-radius: ${p => p.theme.borderRadius};
overflow: hidden;
`;
const WithPadding = styled('div')`
display: flex;
padding: ${space(1)} ${space(2)};
`;
const WordBreak = styled('div')`
word-break: break-word;
`;
const StyledClippedBox = styled(ClippedBox)`
padding: 0;
> div > div {
z-index: 1;
}
`;