content.tsx 27 KB

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