metricSamplesTable.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {LocationDescriptorObject} from 'history';
  4. import debounce from 'lodash/debounce';
  5. import {Button, LinkButton} from 'sentry/components/button';
  6. import {Flex} from 'sentry/components/container/flex';
  7. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  8. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  9. import GridEditable, {
  10. COL_WIDTH_UNDEFINED,
  11. type GridColumnOrder,
  12. } from 'sentry/components/gridEditable';
  13. import SortLink from 'sentry/components/gridEditable/sortLink';
  14. import {Hovercard} from 'sentry/components/hovercard';
  15. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  16. import Link from 'sentry/components/links/link';
  17. import type {SelectionRange} from 'sentry/components/metrics/chart/types';
  18. import PerformanceDuration from 'sentry/components/performanceDuration';
  19. import SmartSearchBar from 'sentry/components/smartSearchBar';
  20. import {Tooltip} from 'sentry/components/tooltip';
  21. import {IconProfiling} from 'sentry/icons';
  22. import {t} from 'sentry/locale';
  23. import {space} from 'sentry/styles/space';
  24. import type {DateString, PageFilters} from 'sentry/types/core';
  25. import type {MetricAggregation, MRI, ParsedMRI} from 'sentry/types/metrics';
  26. import {defined} from 'sentry/utils';
  27. import {trackAnalytics} from 'sentry/utils/analytics';
  28. import {Container, FieldDateTime, NumberContainer} from 'sentry/utils/discover/styles';
  29. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  30. import {getShortEventId} from 'sentry/utils/events';
  31. import {formatMetricUsingUnit} from 'sentry/utils/metrics/formatters';
  32. import {parseMRI} from 'sentry/utils/metrics/mri';
  33. import {
  34. type Field as SelectedField,
  35. getSummaryValueForAggregation,
  36. type MetricsSamplesResults,
  37. type ResultField,
  38. type Summary,
  39. useMetricsSamples,
  40. } from 'sentry/utils/metrics/useMetricsSamples';
  41. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  42. import Projects from 'sentry/utils/projects';
  43. import {decodeScalar} from 'sentry/utils/queryString';
  44. import {useLocation} from 'sentry/utils/useLocation';
  45. import useOrganization from 'sentry/utils/useOrganization';
  46. import usePageFilters from 'sentry/utils/usePageFilters';
  47. import useProjects from 'sentry/utils/useProjects';
  48. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceMetadataHeader';
  49. import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
  50. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  51. import ColorBar from 'sentry/views/performance/vitalDetail/colorBar';
  52. const fields: SelectedField[] = [
  53. 'project',
  54. 'id',
  55. 'span.op',
  56. 'span.description',
  57. 'span.duration',
  58. 'span.self_time',
  59. 'timestamp',
  60. 'trace',
  61. 'transaction',
  62. 'transaction.id',
  63. 'profile.id',
  64. ];
  65. export type Field = (typeof fields)[number];
  66. interface MetricsSamplesTableProps {
  67. aggregation?: MetricAggregation;
  68. focusArea?: SelectionRange;
  69. hasPerformance?: boolean;
  70. mri?: MRI;
  71. onRowHover?: (sampleId?: string) => void;
  72. query?: string;
  73. setMetricsSamples?: React.Dispatch<
  74. React.SetStateAction<MetricsSamplesResults<Field>['data'] | undefined>
  75. >;
  76. sortKey?: string;
  77. }
  78. export function SearchableMetricSamplesTable({
  79. mri,
  80. query: primaryQuery,
  81. ...props
  82. }: MetricsSamplesTableProps) {
  83. const [secondaryQuery, setSecondaryQuery] = useState('');
  84. const handleSearch = useCallback(value => {
  85. setSecondaryQuery(value);
  86. }, []);
  87. const query = useMemo(() => {
  88. if (!secondaryQuery) {
  89. return primaryQuery;
  90. }
  91. return `${primaryQuery} ${secondaryQuery}`;
  92. }, [primaryQuery, secondaryQuery]);
  93. return (
  94. <Fragment>
  95. <MetricsSamplesSearchBar
  96. mri={mri}
  97. query={secondaryQuery}
  98. handleSearch={handleSearch}
  99. />
  100. <MetricSamplesTable mri={mri} query={query} {...props} />
  101. </Fragment>
  102. );
  103. }
  104. interface MetricsSamplesSearchBarProps {
  105. handleSearch: (string) => void;
  106. query: string;
  107. mri?: MRI;
  108. }
  109. export function MetricsSamplesSearchBar({
  110. handleSearch,
  111. mri,
  112. query,
  113. }: MetricsSamplesSearchBarProps) {
  114. const parsedMRI = useMemo(() => {
  115. if (!defined(mri)) {
  116. return null;
  117. }
  118. return parseMRI(mri);
  119. }, [mri]);
  120. const enabled = useMemo(() => {
  121. return parsedMRI?.useCase === 'transactions' || parsedMRI?.useCase === 'spans';
  122. }, [parsedMRI]);
  123. return (
  124. <SearchBar
  125. disabled={!enabled}
  126. query={query}
  127. onSearch={handleSearch}
  128. placeholder={
  129. enabled ? t('Filter by span tags') : t('Search not available for this metric')
  130. }
  131. />
  132. );
  133. }
  134. export function MetricSamplesTable({
  135. focusArea,
  136. mri,
  137. onRowHover,
  138. aggregation,
  139. query,
  140. setMetricsSamples,
  141. sortKey = 'sort',
  142. hasPerformance = true,
  143. }: MetricsSamplesTableProps) {
  144. const location = useLocation();
  145. const enabled = defined(mri);
  146. const parsedMRI = useMemo(() => {
  147. if (!defined(mri)) {
  148. return null;
  149. }
  150. return parseMRI(mri);
  151. }, [mri]);
  152. const datetime = useMemo(() => {
  153. if (!defined(focusArea) || !defined(focusArea.start) || !defined(focusArea.end)) {
  154. return undefined;
  155. }
  156. return {
  157. start: focusArea.start,
  158. end: focusArea.end,
  159. } as PageFilters['datetime'];
  160. }, [focusArea]);
  161. const currentSort = useMemo(() => {
  162. const value = decodeScalar(location.query[sortKey], '');
  163. if (!value) {
  164. return undefined;
  165. }
  166. const direction: 'asc' | 'desc' = value[0] === '-' ? 'desc' : 'asc';
  167. const key = direction === 'asc' ? value : value.substring(1);
  168. if (ALWAYS_SORTABLE_COLUMNS.has(key as ResultField)) {
  169. return {key, direction};
  170. }
  171. if (OPTIONALLY_SORTABLE_COLUMNS.has(key as ResultField)) {
  172. const column = getColumnForMRI(parsedMRI);
  173. if (column.key === key) {
  174. return {key, direction};
  175. }
  176. }
  177. return undefined;
  178. }, [location.query, parsedMRI, sortKey]);
  179. const sortQuery = useMemo(() => {
  180. if (!defined(currentSort)) {
  181. return undefined;
  182. }
  183. const direction = currentSort.direction === 'asc' ? '' : '-';
  184. return `${direction}${currentSort.key}`;
  185. }, [currentSort]);
  186. const result = useMetricsSamples({
  187. fields,
  188. datetime,
  189. max: focusArea?.max,
  190. min: focusArea?.min,
  191. mri,
  192. aggregation,
  193. query,
  194. referrer: 'api.organization.metrics-samples',
  195. enabled,
  196. sort: sortQuery,
  197. limit: 20,
  198. });
  199. // propagate the metrics samples up as needed
  200. useEffect(() => {
  201. setMetricsSamples?.(result.data?.data ?? []);
  202. }, [result?.data?.data, setMetricsSamples]);
  203. const supportedMRI = useMemo(() => {
  204. const responseJSON = result.error?.responseJSON;
  205. if (typeof responseJSON?.detail !== 'string') {
  206. return true;
  207. }
  208. return !responseJSON?.detail?.startsWith('Unsupported MRI: ');
  209. }, [result]);
  210. const emptyMessage = useMemo(() => {
  211. if (!hasPerformance) {
  212. return (
  213. <PerformanceEmptyState withIcon={false}>
  214. <p>{t('You need to set up performance monitoring to collect samples.')}</p>
  215. <LinkButton
  216. priority="primary"
  217. external
  218. href="https://docs.sentry.io/performance-monitoring/getting-started"
  219. >
  220. {t('Set Up Now')}
  221. </LinkButton>
  222. </PerformanceEmptyState>
  223. );
  224. }
  225. if (!defined(mri)) {
  226. return (
  227. <EmptyStateWarning>
  228. <p>{t('Choose a metric to display samples')}</p>
  229. </EmptyStateWarning>
  230. );
  231. }
  232. return null;
  233. }, [mri, hasPerformance]);
  234. const _renderHeadCell = useMemo(() => {
  235. const generateSortLink = (key: string) => () => {
  236. if (!SORTABLE_COLUMNS.has(key as ResultField)) {
  237. return undefined;
  238. }
  239. let sort: string | undefined = undefined;
  240. if (defined(currentSort) && currentSort.key === key) {
  241. if (currentSort.direction === 'desc') {
  242. sort = key;
  243. }
  244. } else {
  245. sort = `-${key}`;
  246. }
  247. return {
  248. ...location,
  249. query: {
  250. ...location.query,
  251. sort,
  252. },
  253. };
  254. };
  255. return renderHeadCell(currentSort, generateSortLink);
  256. }, [currentSort, location]);
  257. const _renderBodyCell = useMemo(
  258. () => renderBodyCell(aggregation, parsedMRI?.unit),
  259. [aggregation, parsedMRI?.unit]
  260. );
  261. const wrapperRef = useRef<HTMLDivElement>(null);
  262. const handleMouseMove = useMemo(
  263. () =>
  264. debounce((event: React.MouseEvent) => {
  265. const wrapper = wrapperRef.current;
  266. const target = event.target;
  267. if (!wrapper || !(target instanceof Element)) {
  268. onRowHover?.(undefined);
  269. return;
  270. }
  271. const tableRow = (target as Element).closest('tbody >tr');
  272. if (!tableRow) {
  273. onRowHover?.(undefined);
  274. return;
  275. }
  276. const rows = Array.from(wrapper.querySelectorAll('tbody > tr'));
  277. const rowIndex = rows.indexOf(tableRow);
  278. const rowId = result.data?.data?.[rowIndex]?.id;
  279. if (!rowId) {
  280. onRowHover?.(undefined);
  281. return;
  282. }
  283. onRowHover?.(rowId);
  284. }, 10),
  285. [onRowHover, result.data?.data]
  286. );
  287. return (
  288. <div
  289. ref={wrapperRef}
  290. onMouseMove={handleMouseMove}
  291. onMouseLeave={() => onRowHover?.(undefined)}
  292. >
  293. <GridEditable
  294. isLoading={enabled && result.isLoading}
  295. error={enabled && result.isError && supportedMRI}
  296. data={result.data?.data ?? []}
  297. columnOrder={getColumnOrder(parsedMRI)}
  298. columnSortBy={[]}
  299. grid={{
  300. renderBodyCell: _renderBodyCell,
  301. renderHeadCell: _renderHeadCell,
  302. }}
  303. emptyMessage={emptyMessage}
  304. minimumColWidth={60}
  305. />
  306. </div>
  307. );
  308. }
  309. function getColumnForMRI(parsedMRI?: ParsedMRI | null): GridColumnOrder<ResultField> {
  310. return parsedMRI?.useCase === 'spans' && parsedMRI?.name === 'span.self_time'
  311. ? {key: 'span.self_time', width: COL_WIDTH_UNDEFINED, name: 'Self Time'}
  312. : parsedMRI?.useCase === 'transactions' && parsedMRI?.name === 'transaction.duration'
  313. ? {key: 'span.duration', width: COL_WIDTH_UNDEFINED, name: 'Duration'}
  314. : {key: 'summary', width: COL_WIDTH_UNDEFINED, name: parsedMRI?.name ?? 'Summary'};
  315. }
  316. function getColumnOrder(parsedMRI?: ParsedMRI | null): GridColumnOrder<ResultField>[] {
  317. const orders: (GridColumnOrder<ResultField> | undefined)[] = [
  318. {key: 'id', width: COL_WIDTH_UNDEFINED, name: 'Span ID'},
  319. {key: 'span.description', width: COL_WIDTH_UNDEFINED, name: 'Description'},
  320. {key: 'span.op', width: COL_WIDTH_UNDEFINED, name: 'Operation'},
  321. getColumnForMRI(parsedMRI),
  322. {key: 'timestamp', width: COL_WIDTH_UNDEFINED, name: 'Timestamp'},
  323. {key: 'profile.id', width: COL_WIDTH_UNDEFINED, name: 'Profile'},
  324. ];
  325. return orders.filter(
  326. (
  327. order: GridColumnOrder<ResultField> | undefined
  328. ): order is GridColumnOrder<ResultField> => !!order
  329. );
  330. }
  331. const RIGHT_ALIGNED_COLUMNS = new Set<ResultField>([
  332. 'span.duration',
  333. 'span.self_time',
  334. 'summary',
  335. ]);
  336. const ALWAYS_SORTABLE_COLUMNS = new Set<ResultField>(['timestamp']);
  337. const OPTIONALLY_SORTABLE_COLUMNS = new Set<ResultField>([
  338. 'summary',
  339. 'span.self_time',
  340. 'span.duration',
  341. ]);
  342. const SORTABLE_COLUMNS: Set<ResultField> = new Set([
  343. ...ALWAYS_SORTABLE_COLUMNS,
  344. ...OPTIONALLY_SORTABLE_COLUMNS,
  345. ]);
  346. function renderHeadCell(
  347. currentSort: {direction: 'asc' | 'desc'; key: string} | undefined,
  348. generateSortLink: (key) => () => LocationDescriptorObject | undefined
  349. ) {
  350. return function (col: GridColumnOrder<ResultField>) {
  351. return (
  352. <SortLink
  353. align={RIGHT_ALIGNED_COLUMNS.has(col.key) ? 'right' : 'left'}
  354. canSort={SORTABLE_COLUMNS.has(col.key)}
  355. direction={col.key === currentSort?.key ? currentSort?.direction : undefined}
  356. generateSortLink={generateSortLink(col.key)}
  357. title={col.name}
  358. />
  359. );
  360. };
  361. }
  362. function renderBodyCell(aggregation?: MetricAggregation, unit?: string) {
  363. return function (
  364. col: GridColumnOrder<ResultField>,
  365. dataRow: MetricsSamplesResults<SelectedField>['data'][number]
  366. ) {
  367. if (col.key === 'id') {
  368. return (
  369. <SpanId
  370. project={dataRow.project}
  371. trace={dataRow.trace}
  372. timestamp={dataRow.timestamp}
  373. selfTime={dataRow['span.self_time']}
  374. duration={dataRow['span.duration']}
  375. spanId={dataRow.id}
  376. transaction={dataRow.transaction}
  377. transactionId={dataRow['transaction.id']}
  378. />
  379. );
  380. }
  381. if (col.key === 'span.description') {
  382. return (
  383. <SpanDescription
  384. description={dataRow['span.description']}
  385. project={dataRow.project}
  386. />
  387. );
  388. }
  389. if (col.key === 'span.self_time' || col.key === 'span.duration') {
  390. return <DurationRenderer duration={dataRow[col.key]} />;
  391. }
  392. if (col.key === 'summary') {
  393. return (
  394. <SummaryRenderer
  395. summary={dataRow.summary}
  396. aggregation={aggregation}
  397. unit={unit}
  398. />
  399. );
  400. }
  401. if (col.key === 'timestamp') {
  402. return <TimestampRenderer timestamp={dataRow.timestamp} />;
  403. }
  404. if (col.key === 'trace') {
  405. return (
  406. <TraceId
  407. traceId={dataRow.trace}
  408. timestamp={dataRow.timestamp}
  409. eventId={dataRow.id}
  410. />
  411. );
  412. }
  413. if (col.key === 'profile.id') {
  414. return (
  415. <ProfileId projectSlug={dataRow.project} profileId={dataRow['profile.id']} />
  416. );
  417. }
  418. return <Container>{dataRow[col.key]}</Container>;
  419. };
  420. }
  421. function ProjectRenderer({projectSlug}: {projectSlug: string}) {
  422. const organization = useOrganization();
  423. return (
  424. <Flex>
  425. <Projects orgId={organization.slug} slugs={[projectSlug]}>
  426. {({projects}) => {
  427. const project = projects.find(p => p.slug === projectSlug);
  428. return (
  429. <ProjectBadge
  430. project={project ? project : {slug: projectSlug}}
  431. avatarSize={16}
  432. hideName
  433. />
  434. );
  435. }}
  436. </Projects>
  437. </Flex>
  438. );
  439. }
  440. function SpanId({
  441. duration,
  442. project,
  443. selfTime,
  444. spanId,
  445. transaction,
  446. transactionId,
  447. trace,
  448. timestamp,
  449. selfTimeColor = '#694D99',
  450. durationColor = 'gray100',
  451. }: {
  452. duration: number;
  453. project: string;
  454. selfTime: number;
  455. spanId: string;
  456. timestamp: DateString;
  457. trace: string;
  458. transaction: string;
  459. transactionId: string | null;
  460. durationColor?: string;
  461. selfTimeColor?: string;
  462. }) {
  463. const location = useLocation();
  464. const organization = useOrganization();
  465. const {projects} = useProjects({slugs: [project]});
  466. const transactionDetailsTarget = defined(transactionId)
  467. ? generateLinkToEventInTraceView({
  468. eventId: transactionId,
  469. projectSlug: project,
  470. traceSlug: trace,
  471. timestamp: timestamp?.toString() ?? '',
  472. location,
  473. organization,
  474. spanId,
  475. transactionName: transaction,
  476. source: TraceViewSources.METRICS,
  477. })
  478. : undefined;
  479. const colorStops = useMemo(() => {
  480. const percentage = selfTime / duration;
  481. return [
  482. {color: selfTimeColor, percent: percentage},
  483. {color: durationColor, percent: 1 - percentage},
  484. ];
  485. }, [duration, selfTime, durationColor, selfTimeColor]);
  486. const transactionSummaryTarget = transactionSummaryRouteWithQuery({
  487. orgSlug: organization.slug,
  488. transaction,
  489. query: {
  490. ...location.query,
  491. query: undefined,
  492. },
  493. projectID: String(projects[0]?.id ?? ''),
  494. });
  495. let contents = spanId ? (
  496. <Fragment>{getShortEventId(spanId)}</Fragment>
  497. ) : (
  498. <EmptyValueContainer>{t('(no value)')}</EmptyValueContainer>
  499. );
  500. if (defined(transactionDetailsTarget)) {
  501. contents = <Link to={transactionDetailsTarget}>{getShortEventId(spanId)}</Link>;
  502. }
  503. return (
  504. <Container>
  505. <StyledHovercard
  506. header={
  507. <Flex justify="space-between" align="center">
  508. {t('Span ID')}
  509. <SpanIdWrapper>
  510. {getShortEventId(spanId)}
  511. <CopyToClipboardButton borderless iconSize="xs" size="zero" text={spanId} />
  512. </SpanIdWrapper>
  513. </Flex>
  514. }
  515. body={
  516. <Flex gap={space(0.75)} column>
  517. <SectionTitle>{t('Duration')}</SectionTitle>
  518. <ColorBar colorStops={colorStops} />
  519. <Flex justify="space-between" align="center">
  520. <Flex justify="space-between" align="center" gap={space(0.5)}>
  521. <LegendDot color={selfTimeColor} />
  522. {t('Self Time: ')}
  523. <PerformanceDuration milliseconds={selfTime} abbreviation />
  524. </Flex>
  525. <Flex justify="space-between" align="center" gap={space(0.5)}>
  526. <LegendDot color={durationColor} />
  527. {t('Duration: ')}
  528. <PerformanceDuration milliseconds={duration} abbreviation />
  529. </Flex>
  530. </Flex>
  531. <SectionTitle>{t('Transaction')}</SectionTitle>
  532. <Tooltip containerDisplayMode="inline" showOnlyOnOverflow title={transaction}>
  533. <Link
  534. to={transactionSummaryTarget}
  535. onClick={() =>
  536. trackAnalytics('ddm.sample-table-interaction', {
  537. organization,
  538. target: 'description',
  539. })
  540. }
  541. >
  542. <TextOverflow>{transaction}</TextOverflow>
  543. </Link>
  544. </Tooltip>
  545. </Flex>
  546. }
  547. showUnderline
  548. >
  549. {contents}
  550. </StyledHovercard>
  551. </Container>
  552. );
  553. }
  554. function SpanDescription({description, project}: {description: string; project: string}) {
  555. if (!description) {
  556. return (
  557. <Flex gap={space(0.75)} align="center">
  558. <ProjectRenderer projectSlug={project} />
  559. <EmptyValueContainer>{t('(none)')}</EmptyValueContainer>
  560. </Flex>
  561. );
  562. }
  563. return (
  564. <Flex gap={space(0.75)} align="center">
  565. <ProjectRenderer projectSlug={project} />
  566. <Container>{description}</Container>
  567. </Flex>
  568. );
  569. }
  570. function DurationRenderer({duration}: {duration: number}) {
  571. return (
  572. <NumberContainer>
  573. <PerformanceDuration milliseconds={duration} abbreviation />
  574. </NumberContainer>
  575. );
  576. }
  577. function SummaryRenderer({
  578. summary,
  579. aggregation,
  580. unit,
  581. }: {
  582. summary: Summary;
  583. aggregation?: MetricAggregation;
  584. unit?: string;
  585. }) {
  586. const value = getSummaryValueForAggregation(summary, aggregation);
  587. // if the op is `count`, then the unit does not apply
  588. unit = aggregation === 'count' ? '' : unit;
  589. return (
  590. <NumberContainer>{formatMetricUsingUnit(value ?? null, unit ?? '')}</NumberContainer>
  591. );
  592. }
  593. function TimestampRenderer({timestamp}: {timestamp: DateString}) {
  594. const location = useLocation();
  595. return (
  596. <FieldDateTime
  597. date={timestamp}
  598. year
  599. seconds
  600. timeZone
  601. utc={decodeScalar(location?.query?.utc) === 'true'}
  602. />
  603. );
  604. }
  605. function TraceId({
  606. traceId,
  607. timestamp,
  608. eventId,
  609. }: {
  610. traceId: string;
  611. eventId?: string;
  612. timestamp?: DateString;
  613. }) {
  614. const organization = useOrganization();
  615. const location = useLocation();
  616. const {selection} = usePageFilters();
  617. const stringOrNumberTimestamp =
  618. timestamp instanceof Date ? timestamp.toISOString() : timestamp ?? '';
  619. const target = getTraceDetailsUrl({
  620. organization,
  621. traceSlug: traceId,
  622. dateSelection: {
  623. start: selection.datetime.start,
  624. end: selection.datetime.end,
  625. statsPeriod: selection.datetime.period,
  626. },
  627. timestamp: stringOrNumberTimestamp,
  628. eventId,
  629. location,
  630. source: TraceViewSources.METRICS,
  631. });
  632. return (
  633. <Container>
  634. <Link
  635. to={target}
  636. onClick={() =>
  637. trackAnalytics('ddm.sample-table-interaction', {
  638. organization,
  639. target: 'trace-id',
  640. })
  641. }
  642. >
  643. {getShortEventId(traceId)}
  644. </Link>
  645. </Container>
  646. );
  647. }
  648. function ProfileId({
  649. profileId,
  650. projectSlug,
  651. }: {
  652. profileId: string | null;
  653. projectSlug: string;
  654. }) {
  655. const organization = useOrganization();
  656. if (!defined(profileId)) {
  657. return (
  658. <Container>
  659. <Button href={undefined} disabled size="xs">
  660. <IconProfiling size="xs" />
  661. </Button>
  662. </Container>
  663. );
  664. }
  665. const target = generateProfileFlamechartRoute({
  666. orgSlug: organization.slug,
  667. projectSlug,
  668. profileId,
  669. });
  670. return (
  671. <Container>
  672. <LinkButton
  673. to={target}
  674. size="xs"
  675. onClick={() =>
  676. trackAnalytics('ddm.sample-table-interaction', {
  677. organization,
  678. target: 'profile',
  679. })
  680. }
  681. >
  682. <IconProfiling size="xs" />
  683. </LinkButton>
  684. </Container>
  685. );
  686. }
  687. const SearchBar = styled(SmartSearchBar)`
  688. margin-bottom: ${space(2)};
  689. `;
  690. const StyledHovercard = styled(Hovercard)`
  691. width: 350px;
  692. `;
  693. const SpanIdWrapper = styled('span')`
  694. font-weight: ${p => p.theme.fontWeightNormal};
  695. `;
  696. const SectionTitle = styled('h6')`
  697. color: ${p => p.theme.subText};
  698. margin-bottom: 0;
  699. `;
  700. const TextOverflow = styled('span')`
  701. ${p => p.theme.overflowEllipsis};
  702. `;
  703. const LegendDot = styled('div')<{color: string}>`
  704. display: block;
  705. width: ${space(1)};
  706. height: ${space(1)};
  707. border-radius: 100%;
  708. background-color: ${p => p.theme[p.color] ?? p.color};
  709. `;
  710. const EmptyValueContainer = styled('span')`
  711. color: ${p => p.theme.gray300};
  712. `;
  713. const PerformanceEmptyState = styled(EmptyStateWarning)`
  714. font-size: ${p => p.theme.fontSizeExtraLarge};
  715. `;