content.tsx 23 KB

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