content.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import debounce from 'lodash/debounce';
  5. import omit from 'lodash/omit';
  6. import {Alert} from 'sentry/components/alert';
  7. import {Button} from 'sentry/components/button';
  8. import Count from 'sentry/components/count';
  9. import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import LoadingIndicator from 'sentry/components/loadingIndicator';
  13. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  14. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  15. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  16. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  17. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  18. import Panel from 'sentry/components/panels/panel';
  19. import PanelHeader from 'sentry/components/panels/panelHeader';
  20. import PanelItem from 'sentry/components/panels/panelItem';
  21. import PerformanceDuration from 'sentry/components/performanceDuration';
  22. import {Tooltip} from 'sentry/components/tooltip';
  23. import {IconChevron} from 'sentry/icons/iconChevron';
  24. import {IconClose} from 'sentry/icons/iconClose';
  25. import {IconWarning} from 'sentry/icons/iconWarning';
  26. import {t, tct} from 'sentry/locale';
  27. import {space} from 'sentry/styles/space';
  28. import type {PageFilters} from 'sentry/types/core';
  29. import type {MRI} from 'sentry/types/metrics';
  30. import type {Organization} from 'sentry/types/organization';
  31. import {trackAnalytics} from 'sentry/utils/analytics';
  32. import {browserHistory} from 'sentry/utils/browserHistory';
  33. import {getFormattedMQL} from 'sentry/utils/metrics';
  34. import {useApiQuery} from 'sentry/utils/queryClient';
  35. import {decodeInteger, decodeList, decodeScalar} from 'sentry/utils/queryString';
  36. import {useLocation} from 'sentry/utils/useLocation';
  37. import useOrganization from 'sentry/utils/useOrganization';
  38. import usePageFilters from 'sentry/utils/usePageFilters';
  39. import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
  40. import {type Field, FIELDS, SORTS} from './data';
  41. import {
  42. BREAKDOWN_SLICES,
  43. ProjectRenderer,
  44. SpanBreakdownSliceRenderer,
  45. SpanDescriptionRenderer,
  46. SpanIdRenderer,
  47. SpanTimeRenderer,
  48. TraceBreakdownContainer,
  49. TraceBreakdownRenderer,
  50. TraceIdRenderer,
  51. TraceIssuesRenderer,
  52. } from './fieldRenderers';
  53. import {TracesChart} from './tracesChart';
  54. import {TracesSearchBar} from './tracesSearchBar';
  55. import {
  56. ALL_PROJECTS,
  57. areQueriesEmpty,
  58. getSecondaryNameFromSpan,
  59. getStylingSliceName,
  60. normalizeTraces,
  61. } from './utils';
  62. const DEFAULT_PER_PAGE = 50;
  63. const SPAN_PROPS_DOCS_URL =
  64. 'https://docs.sentry.io/concepts/search/searchable-properties/spans/';
  65. function usePageParams(location) {
  66. const queries = useMemo(() => {
  67. return decodeList(location.query.query);
  68. }, [location.query.query]);
  69. const metricsMax = decodeScalar(location.query.metricsMax);
  70. const metricsMin = decodeScalar(location.query.metricsMin);
  71. const metricsOp = decodeScalar(location.query.metricsOp);
  72. const metricsQuery = decodeScalar(location.query.metricsQuery);
  73. const mri = decodeScalar(location.query.mri);
  74. return {
  75. queries,
  76. metricsMax,
  77. metricsMin,
  78. metricsOp,
  79. metricsQuery,
  80. mri,
  81. };
  82. }
  83. export function Content() {
  84. const location = useLocation();
  85. const limit = useMemo(() => {
  86. return decodeInteger(location.query.perPage, DEFAULT_PER_PAGE);
  87. }, [location.query.perPage]);
  88. const {queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri} =
  89. usePageParams(location);
  90. const hasMetric = metricsOp && mri;
  91. const removeMetric = useCallback(() => {
  92. browserHistory.push({
  93. ...location,
  94. query: omit(location.query, [
  95. 'mri',
  96. 'metricsOp',
  97. 'metricsQuery',
  98. 'metricsMax',
  99. 'metricsMin',
  100. ]),
  101. });
  102. }, [location]);
  103. const handleSearch = useCallback(
  104. (searchIndex: number, searchQuery: string) => {
  105. const newQueries = [...queries];
  106. if (newQueries.length === 0) {
  107. // In the odd case someone wants to add search bars before any query has been made, we add both the default one shown and a new one.
  108. newQueries[0] = '';
  109. }
  110. newQueries[searchIndex] = searchQuery;
  111. browserHistory.push({
  112. ...location,
  113. query: {
  114. ...location.query,
  115. cursor: undefined,
  116. query: typeof searchQuery === 'string' ? newQueries : queries,
  117. },
  118. });
  119. },
  120. [location, queries]
  121. );
  122. const handleClearSearch = useCallback(
  123. (searchIndex: number) => {
  124. const newQueries = [...queries];
  125. if (typeof newQueries[searchIndex] !== undefined) {
  126. delete newQueries[searchIndex];
  127. browserHistory.push({
  128. ...location,
  129. query: {
  130. ...location.query,
  131. cursor: undefined,
  132. query: newQueries,
  133. },
  134. });
  135. return true;
  136. }
  137. return false;
  138. },
  139. [location, queries]
  140. );
  141. const tracesQuery = useTraces<Field>({
  142. fields: [
  143. ...FIELDS,
  144. ...SORTS.map(field =>
  145. field.startsWith('-') ? (field.substring(1) as Field) : (field as Field)
  146. ),
  147. ],
  148. limit,
  149. query: queries,
  150. sort: SORTS,
  151. mri: hasMetric ? mri : undefined,
  152. metricsMax: hasMetric ? metricsMax : undefined,
  153. metricsMin: hasMetric ? metricsMin : undefined,
  154. metricsOp: hasMetric ? metricsOp : undefined,
  155. metricsQuery: hasMetric ? metricsQuery : undefined,
  156. });
  157. const isLoading = tracesQuery.isFetching;
  158. const isError = !isLoading && tracesQuery.isError;
  159. const isEmpty = !isLoading && !isError && (tracesQuery?.data?.data?.length ?? 0) === 0;
  160. const data = normalizeTraces(
  161. !isLoading && !isError ? tracesQuery?.data?.data : undefined
  162. );
  163. return (
  164. <LayoutMain fullWidth>
  165. <PageFilterBar condensed>
  166. <Tooltip
  167. title={tct(
  168. "Traces stem across multiple projects. You'll need to narrow down which projects you'd like to include per span.[br](ex. [code:project:javascript])",
  169. {
  170. br: <br />,
  171. code: <Code />,
  172. }
  173. )}
  174. position="bottom"
  175. >
  176. <ProjectPageFilter disabled projectOverride={ALL_PROJECTS} />
  177. </Tooltip>
  178. <EnvironmentPageFilter />
  179. <DatePageFilter defaultPeriod="2h" />
  180. </PageFilterBar>
  181. {hasMetric && (
  182. <StyledAlert
  183. type="info"
  184. showIcon
  185. trailingItems={<StyledCloseButton onClick={removeMetric} />}
  186. >
  187. {tct('The metric query [metricQuery] is filtering the results below.', {
  188. metricQuery: (
  189. <strong>
  190. {getFormattedMQL({mri: mri as MRI, op: metricsOp, query: metricsQuery})}
  191. </strong>
  192. ),
  193. })}
  194. </StyledAlert>
  195. )}
  196. {isError && typeof tracesQuery.error?.responseJSON?.detail === 'string' ? (
  197. <StyledAlert type="error" showIcon>
  198. {tracesQuery.error?.responseJSON?.detail}
  199. </StyledAlert>
  200. ) : null}
  201. <TracesSearchBar
  202. queries={queries}
  203. handleSearch={handleSearch}
  204. handleClearSearch={handleClearSearch}
  205. />
  206. <ModuleLayout.Full>
  207. <TracesChart />
  208. </ModuleLayout.Full>
  209. <StyledPanel>
  210. <TracePanelContent>
  211. <StyledPanelHeader align="left" lightText>
  212. {t('Trace ID')}
  213. </StyledPanelHeader>
  214. <StyledPanelHeader align="left" lightText>
  215. {t('Trace Root')}
  216. </StyledPanelHeader>
  217. <StyledPanelHeader align="right" lightText>
  218. {areQueriesEmpty(queries) ? t('Total Spans') : t('Matching Spans')}
  219. </StyledPanelHeader>
  220. <StyledPanelHeader align="left" lightText>
  221. {t('Timeline')}
  222. </StyledPanelHeader>
  223. <StyledPanelHeader align="right" lightText>
  224. {t('Duration')}
  225. </StyledPanelHeader>
  226. <StyledPanelHeader align="right" lightText>
  227. {t('Timestamp')}
  228. </StyledPanelHeader>
  229. <StyledPanelHeader align="right" lightText>
  230. {t('Issues')}
  231. </StyledPanelHeader>
  232. {isLoading && (
  233. <StyledPanelItem span={7} overflow>
  234. <LoadingIndicator />
  235. </StyledPanelItem>
  236. )}
  237. {isError && ( // TODO: need an error state
  238. <StyledPanelItem span={7} overflow>
  239. <EmptyStreamWrapper>
  240. <IconWarning color="gray300" size="lg" />
  241. </EmptyStreamWrapper>
  242. </StyledPanelItem>
  243. )}
  244. {isEmpty && (
  245. <StyledPanelItem span={7} overflow>
  246. <EmptyStateWarning withIcon>
  247. <EmptyStateText size="fontSizeExtraLarge">
  248. {t('No trace results found')}
  249. </EmptyStateText>
  250. <EmptyStateText size="fontSizeMedium">
  251. {tct('Try adjusting your filters or refer to [docSearchProps].', {
  252. docSearchProps: (
  253. <ExternalLink href={SPAN_PROPS_DOCS_URL}>
  254. {t('docs for search properties')}
  255. </ExternalLink>
  256. ),
  257. })}
  258. </EmptyStateText>
  259. </EmptyStateWarning>
  260. </StyledPanelItem>
  261. )}
  262. {data?.map((trace, i) => (
  263. <TraceRow key={trace.trace} trace={trace} defaultExpanded={i === 0} />
  264. ))}
  265. </TracePanelContent>
  266. </StyledPanel>
  267. </LayoutMain>
  268. );
  269. }
  270. function TraceRow({defaultExpanded, trace}: {defaultExpanded; trace: TraceResult}) {
  271. const [expanded, setExpanded] = useState<boolean>(defaultExpanded);
  272. const [highlightedSliceName, _setHighlightedSliceName] = useState('');
  273. const location = useLocation();
  274. const organization = useOrganization();
  275. const queries = useMemo(() => {
  276. return decodeList(location.query.query);
  277. }, [location.query.query]);
  278. const setHighlightedSliceName = useMemo(
  279. () =>
  280. debounce(sliceName => _setHighlightedSliceName(sliceName), 100, {
  281. leading: true,
  282. }),
  283. [_setHighlightedSliceName]
  284. );
  285. const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]);
  286. return (
  287. <Fragment>
  288. <StyledPanelItem align="center" center onClick={onClickExpand}>
  289. <Button
  290. icon={<IconChevron size="xs" direction={expanded ? 'down' : 'right'} />}
  291. aria-label={t('Toggle trace details')}
  292. aria-expanded={expanded}
  293. size="zero"
  294. borderless
  295. onClick={() =>
  296. trackAnalytics('trace_explorer.toggle_trace_details', {
  297. organization,
  298. expanded,
  299. })
  300. }
  301. />
  302. <TraceIdRenderer
  303. traceId={trace.trace}
  304. timestamp={trace.end}
  305. onClick={() =>
  306. trackAnalytics('trace_explorer.open_trace', {
  307. organization,
  308. })
  309. }
  310. location={location}
  311. />
  312. </StyledPanelItem>
  313. <StyledPanelItem align="left" overflow>
  314. <Description>
  315. {trace.project ? (
  316. <ProjectRenderer projectSlug={trace.project} hideName />
  317. ) : null}
  318. {trace.name ? (
  319. <WrappingText>{trace.name}</WrappingText>
  320. ) : (
  321. <EmptyValueContainer>{t('Missing Trace Root')}</EmptyValueContainer>
  322. )}
  323. </Description>
  324. </StyledPanelItem>
  325. <StyledPanelItem align="right">
  326. {areQueriesEmpty(queries) ? (
  327. <Count value={trace.numSpans} />
  328. ) : (
  329. tct('[numerator][space]of[space][denominator]', {
  330. numerator: <Count value={trace.matchingSpans} />,
  331. denominator: <Count value={trace.numSpans} />,
  332. space: <Fragment>&nbsp;</Fragment>,
  333. })
  334. )}
  335. </StyledPanelItem>
  336. <BreakdownPanelItem
  337. align="right"
  338. highlightedSliceName={highlightedSliceName}
  339. onMouseLeave={() => setHighlightedSliceName('')}
  340. >
  341. <TraceBreakdownRenderer
  342. trace={trace}
  343. setHighlightedSliceName={setHighlightedSliceName}
  344. />
  345. </BreakdownPanelItem>
  346. <StyledPanelItem align="right">
  347. <PerformanceDuration milliseconds={trace.duration} abbreviation />
  348. </StyledPanelItem>
  349. <StyledPanelItem align="right">
  350. <SpanTimeRenderer timestamp={trace.end} tooltipShowSeconds />
  351. </StyledPanelItem>
  352. <StyledPanelItem align="right">
  353. <TraceIssuesRenderer
  354. trace={trace}
  355. onClick={() =>
  356. trackAnalytics('trace_explorer.open_in_issues', {
  357. organization,
  358. })
  359. }
  360. />
  361. </StyledPanelItem>
  362. {expanded && (
  363. <SpanTable trace={trace} setHighlightedSliceName={setHighlightedSliceName} />
  364. )}
  365. </Fragment>
  366. );
  367. }
  368. function SpanTable({
  369. trace,
  370. setHighlightedSliceName,
  371. }: {
  372. setHighlightedSliceName: (sliceName: string) => void;
  373. trace: TraceResult;
  374. }) {
  375. const location = useLocation();
  376. const organization = useOrganization();
  377. const {queries, metricsMax, metricsMin, metricsOp, metricsQuery, mri} =
  378. usePageParams(location);
  379. const hasMetric = metricsOp && mri;
  380. const spansQuery = useTraceSpans({
  381. trace,
  382. fields: [
  383. ...FIELDS,
  384. ...SORTS.map(field =>
  385. field.startsWith('-') ? (field.substring(1) as Field) : (field as Field)
  386. ),
  387. ],
  388. limit: 10,
  389. query: queries,
  390. sort: SORTS,
  391. mri: hasMetric ? mri : undefined,
  392. metricsMax: hasMetric ? metricsMax : undefined,
  393. metricsMin: hasMetric ? metricsMin : undefined,
  394. metricsOp: hasMetric ? metricsOp : undefined,
  395. metricsQuery: hasMetric ? metricsQuery : undefined,
  396. });
  397. const isLoading = spansQuery.isFetching;
  398. const isError = !isLoading && spansQuery.isError;
  399. const hasData = !isLoading && !isError && (spansQuery?.data?.data?.length ?? 0) > 0;
  400. const spans = spansQuery.data?.data ?? [];
  401. return (
  402. <SpanTablePanelItem span={7} overflow>
  403. <StyledPanel>
  404. <SpanPanelContent>
  405. <StyledPanelHeader align="left" lightText>
  406. {t('Span ID')}
  407. </StyledPanelHeader>
  408. <StyledPanelHeader align="left" lightText>
  409. {t('Span Description')}
  410. </StyledPanelHeader>
  411. <StyledPanelHeader align="right" lightText />
  412. <StyledPanelHeader align="right" lightText>
  413. {t('Span Duration')}
  414. </StyledPanelHeader>
  415. <StyledPanelHeader align="right" lightText>
  416. {t('Timestamp')}
  417. </StyledPanelHeader>
  418. {isLoading && (
  419. <StyledPanelItem span={5} overflow>
  420. <LoadingIndicator />
  421. </StyledPanelItem>
  422. )}
  423. {isError && ( // TODO: need an error state
  424. <StyledPanelItem span={5} overflow>
  425. <EmptyStreamWrapper>
  426. <IconWarning color="gray300" size="lg" />
  427. </EmptyStreamWrapper>
  428. </StyledPanelItem>
  429. )}
  430. {spans.map(span => (
  431. <SpanRow
  432. organization={organization}
  433. key={span.id}
  434. span={span}
  435. trace={trace}
  436. setHighlightedSliceName={setHighlightedSliceName}
  437. />
  438. ))}
  439. {hasData && spans.length < trace.matchingSpans && (
  440. <MoreMatchingSpans span={5}>
  441. {tct('[more][space]more [matching]spans can be found in the trace.', {
  442. more: <Count value={trace.matchingSpans - spans.length} />,
  443. space: <Fragment>&nbsp;</Fragment>,
  444. matching: areQueriesEmpty(queries) ? '' : 'matching ',
  445. })}
  446. </MoreMatchingSpans>
  447. )}
  448. </SpanPanelContent>
  449. </StyledPanel>
  450. </SpanTablePanelItem>
  451. );
  452. }
  453. function SpanRow({
  454. organization,
  455. span,
  456. trace,
  457. setHighlightedSliceName,
  458. }: {
  459. organization: Organization;
  460. setHighlightedSliceName: (sliceName: string) => void;
  461. span: SpanResult<Field>;
  462. trace: TraceResult;
  463. }) {
  464. const theme = useTheme();
  465. return (
  466. <Fragment>
  467. <StyledSpanPanelItem align="right">
  468. <SpanIdRenderer
  469. projectSlug={span.project}
  470. transactionId={span['transaction.id']}
  471. spanId={span.id}
  472. traceId={trace.trace}
  473. timestamp={span.timestamp}
  474. onClick={() =>
  475. trackAnalytics('trace_explorer.open_trace_span', {
  476. organization,
  477. })
  478. }
  479. />
  480. </StyledSpanPanelItem>
  481. <StyledSpanPanelItem align="left" overflow>
  482. <SpanDescriptionRenderer span={span} />
  483. </StyledSpanPanelItem>
  484. <StyledSpanPanelItem align="right" onMouseLeave={() => setHighlightedSliceName('')}>
  485. <TraceBreakdownContainer>
  486. <SpanBreakdownSliceRenderer
  487. sliceName={span.project}
  488. sliceSecondaryName={getSecondaryNameFromSpan(span)}
  489. sliceStart={Math.ceil(span['precise.start_ts'] * 1000)}
  490. sliceEnd={Math.floor(span['precise.finish_ts'] * 1000)}
  491. trace={trace}
  492. theme={theme}
  493. onMouseEnter={() =>
  494. setHighlightedSliceName(
  495. getStylingSliceName(span.project, getSecondaryNameFromSpan(span)) ?? ''
  496. )
  497. }
  498. />
  499. </TraceBreakdownContainer>
  500. </StyledSpanPanelItem>
  501. <StyledSpanPanelItem align="right">
  502. <PerformanceDuration milliseconds={span['span.duration']} abbreviation />
  503. </StyledSpanPanelItem>
  504. <StyledSpanPanelItem align="right">
  505. <SpanTimeRenderer
  506. timestamp={span['precise.finish_ts'] * 1000}
  507. tooltipShowSeconds
  508. />
  509. </StyledSpanPanelItem>
  510. </Fragment>
  511. );
  512. }
  513. export type SpanResult<F extends string> = Record<F, any>;
  514. export interface TraceResult {
  515. breakdowns: TraceBreakdownResult[];
  516. duration: number;
  517. end: number;
  518. matchingSpans: number;
  519. name: string | null;
  520. numErrors: number;
  521. numOccurrences: number;
  522. numSpans: number;
  523. project: string | null;
  524. slices: number;
  525. start: number;
  526. trace: string;
  527. }
  528. interface TraceBreakdownBase {
  529. duration: number; // Contains the accurate duration for display. Start and end may be quantized.
  530. end: number;
  531. opCategory: string | null;
  532. sdkName: string | null;
  533. sliceEnd: number;
  534. sliceStart: number;
  535. sliceWidth: number;
  536. start: number;
  537. }
  538. type TraceBreakdownProject = TraceBreakdownBase & {
  539. kind: 'project';
  540. project: string;
  541. };
  542. type TraceBreakdownMissing = TraceBreakdownBase & {
  543. kind: 'missing';
  544. project: null;
  545. };
  546. export type TraceBreakdownResult = TraceBreakdownProject | TraceBreakdownMissing;
  547. interface TraceResults {
  548. data: TraceResult[];
  549. meta: any;
  550. }
  551. interface UseTracesOptions<F extends string> {
  552. fields: F[];
  553. datetime?: PageFilters['datetime'];
  554. enabled?: boolean;
  555. limit?: number;
  556. metricsMax?: string;
  557. metricsMin?: string;
  558. metricsOp?: string;
  559. metricsQuery?: string;
  560. mri?: string;
  561. query?: string | string[];
  562. sort?: string[];
  563. suggestedQuery?: string;
  564. }
  565. function useTraces<F extends string>({
  566. fields,
  567. datetime,
  568. enabled,
  569. limit,
  570. mri,
  571. metricsMax,
  572. metricsMin,
  573. metricsOp,
  574. metricsQuery,
  575. query,
  576. suggestedQuery,
  577. sort,
  578. }: UseTracesOptions<F>) {
  579. const organization = useOrganization();
  580. const {selection} = usePageFilters();
  581. const path = `/organizations/${organization.slug}/traces/`;
  582. const endpointOptions = {
  583. query: {
  584. project: selection.projects,
  585. environment: selection.environments,
  586. ...(datetime ?? normalizeDateTimeParams(selection.datetime)),
  587. field: fields,
  588. query,
  589. suggestedQuery,
  590. sort,
  591. per_page: limit,
  592. breakdownSlices: BREAKDOWN_SLICES,
  593. maxSpansPerTrace: 10,
  594. mri,
  595. metricsMax,
  596. metricsMin,
  597. metricsOp,
  598. metricsQuery,
  599. },
  600. };
  601. const serializedEndpointOptions = JSON.stringify(endpointOptions);
  602. let queries: string[] = [];
  603. if (Array.isArray(query)) {
  604. queries = query;
  605. } else if (query !== undefined) {
  606. queries = [query];
  607. }
  608. useEffect(() => {
  609. trackAnalytics('trace_explorer.search_request', {
  610. organization,
  611. queries,
  612. });
  613. // `queries` is already included as a dep in serializedEndpointOptions
  614. // eslint-disable-next-line react-hooks/exhaustive-deps
  615. }, [serializedEndpointOptions, organization]);
  616. const result = useApiQuery<TraceResults>([path, endpointOptions], {
  617. staleTime: 0,
  618. refetchOnWindowFocus: false,
  619. retry: false,
  620. enabled,
  621. });
  622. useEffect(() => {
  623. if (result.status === 'success') {
  624. trackAnalytics('trace_explorer.search_success', {
  625. organization,
  626. queries,
  627. has_data: result.data.data.length > 0,
  628. num_traces: result.data.data.length,
  629. num_missing_trace_root: result.data.data.filter(trace => trace.name === null)
  630. .length,
  631. });
  632. } else if (result.status === 'error') {
  633. const response = result.error.responseJSON;
  634. const error =
  635. typeof response?.detail === 'string'
  636. ? response?.detail
  637. : response?.detail?.message;
  638. trackAnalytics('trace_explorer.search_failure', {
  639. organization,
  640. queries,
  641. error: error ?? '',
  642. });
  643. }
  644. // result.status is tied to result.data. No need to explicitly
  645. // include result.data as an additional dep.
  646. // eslint-disable-next-line react-hooks/exhaustive-deps
  647. }, [serializedEndpointOptions, result.status, organization]);
  648. return result;
  649. }
  650. interface SpanResults<F extends string> {
  651. data: SpanResult<F>[];
  652. meta: any;
  653. }
  654. interface UseTraceSpansOptions<F extends string> {
  655. fields: F[];
  656. trace: TraceResult;
  657. datetime?: PageFilters['datetime'];
  658. enabled?: boolean;
  659. limit?: number;
  660. metricsMax?: string;
  661. metricsMin?: string;
  662. metricsOp?: string;
  663. metricsQuery?: string;
  664. mri?: string;
  665. query?: string | string[];
  666. sort?: string[];
  667. }
  668. function useTraceSpans<F extends string>({
  669. fields,
  670. trace,
  671. datetime,
  672. enabled,
  673. limit,
  674. mri,
  675. metricsMax,
  676. metricsMin,
  677. metricsOp,
  678. metricsQuery,
  679. query,
  680. sort,
  681. }: UseTraceSpansOptions<F>) {
  682. const organization = useOrganization();
  683. const {selection} = usePageFilters();
  684. const path = `/organizations/${organization.slug}/trace/${trace.trace}/spans/`;
  685. const endpointOptions = {
  686. query: {
  687. project: selection.projects,
  688. environment: selection.environments,
  689. ...(datetime ?? normalizeDateTimeParams(selection.datetime)),
  690. field: fields,
  691. query,
  692. sort,
  693. per_page: limit,
  694. breakdownSlices: BREAKDOWN_SLICES,
  695. maxSpansPerTrace: 10,
  696. mri,
  697. metricsMax,
  698. metricsMin,
  699. metricsOp,
  700. metricsQuery,
  701. },
  702. };
  703. const result = useApiQuery<SpanResults<F>>([path, endpointOptions], {
  704. staleTime: 0,
  705. refetchOnWindowFocus: false,
  706. retry: false,
  707. enabled,
  708. });
  709. return result;
  710. }
  711. const LayoutMain = styled(Layout.Main)`
  712. display: flex;
  713. flex-direction: column;
  714. gap: ${space(2)};
  715. `;
  716. const StyledPanel = styled(Panel)`
  717. margin-bottom: 0px;
  718. `;
  719. const TracePanelContent = styled('div')`
  720. width: 100%;
  721. display: grid;
  722. grid-template-columns: repeat(1, min-content) auto repeat(2, min-content) 85px 85px 66px;
  723. `;
  724. const SpanPanelContent = styled('div')`
  725. width: 100%;
  726. display: grid;
  727. grid-template-columns: repeat(1, min-content) auto repeat(1, min-content) 133px 85px;
  728. `;
  729. const StyledPanelHeader = styled(PanelHeader)<{align: 'left' | 'right'}>`
  730. white-space: nowrap;
  731. justify-content: ${p => (p.align === 'left' ? 'flex-start' : 'flex-end')};
  732. `;
  733. const EmptyStateText = styled('div')<{size: 'fontSizeExtraLarge' | 'fontSizeMedium'}>`
  734. color: ${p => p.theme.gray300};
  735. font-size: ${p => p.theme[p.size]};
  736. padding-bottom: ${space(1)};
  737. `;
  738. const Description = styled('div')`
  739. ${p => p.theme.overflowEllipsis};
  740. display: flex;
  741. flex-direction: row;
  742. align-items: center;
  743. gap: ${space(1)};
  744. `;
  745. const StyledPanelItem = styled(PanelItem)<{
  746. align?: 'left' | 'center' | 'right';
  747. overflow?: boolean;
  748. span?: number;
  749. }>`
  750. align-items: center;
  751. padding: ${space(1)} ${space(2)};
  752. ${p => (p.align === 'left' ? 'justify-content: flex-start;' : null)}
  753. ${p => (p.align === 'right' ? 'justify-content: flex-end;' : null)}
  754. ${p => (p.overflow ? p.theme.overflowEllipsis : null)};
  755. ${p =>
  756. p.align === 'center'
  757. ? `
  758. justify-content: space-around;`
  759. : p.align === 'left' || p.align === 'right'
  760. ? `text-align: ${p.align};`
  761. : undefined}
  762. ${p => p.span && `grid-column: auto / span ${p.span};`}
  763. white-space: nowrap;
  764. `;
  765. const MoreMatchingSpans = styled(StyledPanelItem)`
  766. color: ${p => p.theme.gray300};
  767. `;
  768. const WrappingText = styled('div')`
  769. width: 100%;
  770. ${p => p.theme.overflowEllipsis};
  771. `;
  772. const StyledSpanPanelItem = styled(StyledPanelItem)`
  773. &:nth-child(10n + 1),
  774. &:nth-child(10n + 2),
  775. &:nth-child(10n + 3),
  776. &:nth-child(10n + 4),
  777. &:nth-child(10n + 5) {
  778. background-color: ${p => p.theme.backgroundSecondary};
  779. }
  780. `;
  781. const SpanTablePanelItem = styled(StyledPanelItem)`
  782. background-color: ${p => p.theme.gray100};
  783. `;
  784. const BreakdownPanelItem = styled(StyledPanelItem)<{highlightedSliceName: string}>`
  785. ${p =>
  786. p.highlightedSliceName
  787. ? `--highlightedSlice-${p.highlightedSliceName}-opacity: 1.0;
  788. --highlightedSlice-${p.highlightedSliceName}-saturate: saturate(1.0) contrast(1.0);
  789. --highlightedSlice-${p.highlightedSliceName}-transform: translateY(0px);
  790. `
  791. : null}
  792. ${p =>
  793. p.highlightedSliceName
  794. ? `
  795. --defaultSlice-opacity: 1.0;
  796. --defaultSlice-saturate: saturate(0.7) contrast(0.9) brightness(1.2);
  797. --defaultSlice-transform: translateY(0px);
  798. `
  799. : `
  800. --defaultSlice-opacity: 1.0;
  801. --defaultSlice-saturate: saturate(1.0) contrast(1.0);
  802. --defaultSlice-transform: translateY(0px);
  803. `}
  804. `;
  805. const EmptyValueContainer = styled('span')`
  806. color: ${p => p.theme.gray300};
  807. `;
  808. const StyledAlert = styled(Alert)`
  809. margin-bottom: 0;
  810. `;
  811. const StyledCloseButton = styled(IconClose)`
  812. cursor: pointer;
  813. `;
  814. const Code = styled('code')`
  815. color: ${p => p.theme.red400};
  816. `;