content.tsx 27 KB

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