content.tsx 27 KB

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