content.tsx 27 KB

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