content.tsx 26 KB

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