newTraceDetailsSpanDetails.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import * as qs from 'query-string';
  5. import {Alert} from 'sentry/components/alert';
  6. import {Button} from 'sentry/components/button';
  7. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  8. import {DateTime} from 'sentry/components/dateTime';
  9. import DiscoverButton from 'sentry/components/discoverButton';
  10. import SpanSummaryButton from 'sentry/components/events/interfaces/spans/spanSummaryButton';
  11. import FileSize from 'sentry/components/fileSize';
  12. import ExternalLink from 'sentry/components/links/externalLink';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import Pill from 'sentry/components/pill';
  15. import Pills from 'sentry/components/pills';
  16. import {TransactionToProfileButton} from 'sentry/components/profiling/transactionToProfileButton';
  17. import QuestionTooltip from 'sentry/components/questionTooltip';
  18. import {ALL_ACCESS_PROJECTS, PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
  19. import {t} from 'sentry/locale';
  20. import {space} from 'sentry/styles/space';
  21. import type {EventTransaction} from 'sentry/types/event';
  22. import type {Organization} from 'sentry/types/organization';
  23. import {assert} from 'sentry/types/utils';
  24. import {defined} from 'sentry/utils';
  25. import EventView from 'sentry/utils/discover/eventView';
  26. import {generateEventSlug} from 'sentry/utils/discover/urls';
  27. import getDynamicText from 'sentry/utils/getDynamicText';
  28. import {safeURL} from 'sentry/utils/url/safeURL';
  29. import {useLocation} from 'sentry/utils/useLocation';
  30. import useProjects from 'sentry/utils/useProjects';
  31. import {CustomMetricsEventData} from 'sentry/views/metrics/customMetricsEventData';
  32. import {IssueList} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/issues/issues';
  33. import {TraceDrawerComponents} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/styles';
  34. import type {
  35. TraceTree,
  36. TraceTreeNode,
  37. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  38. import {getTraceTabTitle} from 'sentry/views/performance/newTraceDetails/traceState/traceTabs';
  39. import {GeneralSpanDetailsValue} from 'sentry/views/performance/traceDetails/newTraceDetailsValueRenderer';
  40. import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
  41. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  42. import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
  43. import {Frame, SpanDescription} from 'sentry/views/starfish/components/spanDescription';
  44. import {FrameContainer} from 'sentry/views/starfish/components/stackTraceMiniFrame';
  45. import {ModuleName} from 'sentry/views/starfish/types';
  46. import {resolveSpanModule} from 'sentry/views/starfish/utils/resolveSpanModule';
  47. import {OpsDot} from '../../opsBreakdown';
  48. import * as SpanEntryContext from './context';
  49. import {GapSpanDetails} from './gapSpanDetails';
  50. import InlineDocs from './inlineDocs';
  51. import {SpanProfileDetails} from './spanProfileDetails';
  52. import type {ParsedTraceType, RawSpanType} from './types';
  53. import {rawSpanKeys} from './types';
  54. import type {SubTimingInfo} from './utils';
  55. import {
  56. getFormattedTimeRangeWithLeadingAndTrailingZero,
  57. getSpanSubTimings,
  58. getTraceDateTimeRange,
  59. isGapSpan,
  60. isHiddenDataKey,
  61. isOrphanSpan,
  62. scrollToSpan,
  63. } from './utils';
  64. const SIZE_DATA_KEYS = [
  65. 'Encoded Body Size',
  66. 'Decoded Body Size',
  67. 'Transfer Size',
  68. 'http.request_content_length',
  69. 'http.response_content_length',
  70. 'http.decoded_response_content_length',
  71. 'http.response_transfer_size',
  72. ];
  73. type TransactionResult = {
  74. id: string;
  75. 'project.name': string;
  76. 'trace.span': string;
  77. transaction: string;
  78. };
  79. export type SpanDetailProps = {
  80. event: Readonly<EventTransaction>;
  81. node: TraceTreeNode<TraceTree.Span>;
  82. onParentClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  83. openPanel: string | undefined;
  84. organization: Organization;
  85. trace: Readonly<ParsedTraceType>;
  86. };
  87. function NewTraceDetailsSpanDetail(props: SpanDetailProps) {
  88. const location = useLocation();
  89. const profileId = props.event.contexts.profile?.profile_id || '';
  90. const issues = useMemo(() => {
  91. return [...props.node.errors, ...props.node.performance_issues];
  92. }, [props.node.errors, props.node.performance_issues]);
  93. const {projects} = useProjects();
  94. const project = projects.find(p => p.id === props.event.projectID);
  95. const resolvedModule: ModuleName = resolveSpanModule(
  96. props.node.value.sentry_tags?.op,
  97. props.node.value.sentry_tags?.category
  98. );
  99. function renderTraversalButton(): React.ReactNode {
  100. if (!props.node.value.childTransactions) {
  101. // TODO: Amend size to use theme when we eventually refactor LoadingIndicator
  102. // 12px is consistent with theme.iconSizes['xs'] but theme returns a string.
  103. return (
  104. <StyledDiscoverButton size="xs" disabled>
  105. <StyledLoadingIndicator size={12} />
  106. </StyledDiscoverButton>
  107. );
  108. }
  109. if (props.node.value.childTransactions.length <= 0) {
  110. return null;
  111. }
  112. const {trace, event, organization} = props;
  113. assert(!isGapSpan(props.node.value));
  114. if (props.node.value.childTransactions.length === 1) {
  115. // Note: This is rendered by renderSpanChild() as a dedicated row
  116. return null;
  117. }
  118. const {start, end} = getTraceDateTimeRange({
  119. start: trace.traceStartTimestamp,
  120. end: trace.traceEndTimestamp,
  121. });
  122. const childrenEventView = EventView.fromSavedQuery({
  123. id: undefined,
  124. name: `Children from Span ID ${props.node.value.span_id}`,
  125. fields: [
  126. 'transaction',
  127. 'project',
  128. 'trace.span',
  129. 'transaction.duration',
  130. 'timestamp',
  131. ],
  132. orderby: '-timestamp',
  133. query: `event.type:transaction trace:${props.node.value.trace_id} trace.parent_span:${props.node.value.span_id}`,
  134. projects: organization.features.includes('global-views')
  135. ? [ALL_ACCESS_PROJECTS]
  136. : [Number(event.projectID)],
  137. version: 2,
  138. start,
  139. end,
  140. });
  141. return (
  142. <StyledDiscoverButton
  143. data-test-id="view-child-transactions"
  144. size="xs"
  145. to={childrenEventView.getResultsViewUrlTarget(organization.slug)}
  146. >
  147. {t('View Children')}
  148. </StyledDiscoverButton>
  149. );
  150. }
  151. function renderSpanChild(): React.ReactNode {
  152. const childTransaction = props.node.value.childTransactions?.[0];
  153. if (!childTransaction) {
  154. return null;
  155. }
  156. const transactionResult: TransactionResult = {
  157. 'project.name': childTransaction.value.project_slug,
  158. transaction: childTransaction.value.transaction,
  159. 'trace.span': childTransaction.value.span_id,
  160. id: childTransaction.value.event_id,
  161. };
  162. const eventSlug = generateSlug(transactionResult);
  163. const viewChildButton = (
  164. <SpanEntryContext.Consumer>
  165. {({getViewChildTransactionTarget}) => {
  166. const to = getViewChildTransactionTarget({
  167. ...transactionResult,
  168. eventSlug,
  169. });
  170. if (!to) {
  171. return null;
  172. }
  173. const target = transactionSummaryRouteWithQuery({
  174. orgSlug: props.organization.slug,
  175. transaction: transactionResult.transaction,
  176. query: omit(location.query, Object.values(PAGE_URL_PARAM)),
  177. projectID: String(childTransaction.value.project_id),
  178. });
  179. return (
  180. <ButtonGroup>
  181. <StyledButton data-test-id="view-child-transaction" size="xs" to={to}>
  182. {t('View Transaction')}
  183. </StyledButton>
  184. <StyledButton size="xs" to={target}>
  185. {t('View Summary')}
  186. </StyledButton>
  187. </ButtonGroup>
  188. );
  189. }}
  190. </SpanEntryContext.Consumer>
  191. );
  192. return (
  193. <Row title={t('Child Transaction')} extra={viewChildButton}>
  194. {`${transactionResult.transaction} (${transactionResult['project.name']})`}
  195. </Row>
  196. );
  197. }
  198. function renderSpanDetailActions() {
  199. const {organization, event} = props;
  200. if (isGapSpan(props.node.value) || !props.node.value.op || !props.node.value.hash) {
  201. return null;
  202. }
  203. const transactionName = event.title;
  204. return (
  205. <ButtonGroup>
  206. <SpanSummaryButton
  207. event={event}
  208. organization={organization}
  209. span={props.node.value}
  210. />
  211. <StyledButton
  212. size="xs"
  213. to={spanDetailsRouteWithQuery({
  214. orgSlug: organization.slug,
  215. transaction: transactionName,
  216. query: location.query,
  217. spanSlug: {op: props.node.value.op, group: props.node.value.hash},
  218. projectID: event.projectID,
  219. })}
  220. >
  221. {t('View Similar Spans')}
  222. </StyledButton>
  223. </ButtonGroup>
  224. );
  225. }
  226. function renderOrphanSpanMessage() {
  227. if (!isOrphanSpan(props.node.value)) {
  228. return null;
  229. }
  230. return (
  231. <Alert type="info" showIcon system>
  232. {t(
  233. 'This is a span that has no parent span within this transaction. It has been attached to the transaction root span by default.'
  234. )}
  235. </Alert>
  236. );
  237. }
  238. function renderSpanErrorMessage() {
  239. const hasErrors =
  240. props.node.errors.size > 0 || props.node.performance_issues.size > 0;
  241. if (!hasErrors || isGapSpan(props.node.value)) {
  242. return null;
  243. }
  244. return (
  245. <IssueList organization={props.organization} issues={issues} node={props.node} />
  246. );
  247. }
  248. function partitionSizes(data): {
  249. nonSizeKeys: {[key: string]: unknown};
  250. sizeKeys: {[key: string]: number};
  251. } {
  252. const sizeKeys = SIZE_DATA_KEYS.reduce((keys, key) => {
  253. if (data.hasOwnProperty(key) && defined(data[key])) {
  254. try {
  255. keys[key] = parseInt(data[key], 10);
  256. } catch (e) {
  257. keys[key] = data[key];
  258. }
  259. }
  260. return keys;
  261. }, {});
  262. const nonSizeKeys = {...data};
  263. SIZE_DATA_KEYS.forEach(key => delete nonSizeKeys[key]);
  264. return {
  265. sizeKeys,
  266. nonSizeKeys,
  267. };
  268. }
  269. function renderProfileMessage() {
  270. if (
  271. !props.organization.features.includes('profiling') ||
  272. isGapSpan(props.node.value)
  273. ) {
  274. return null;
  275. }
  276. return <SpanProfileDetails span={props.node.value} event={props.event} />;
  277. }
  278. function renderSpanDetails() {
  279. const {event, organization} = props;
  280. const span = props.node.value;
  281. if (isGapSpan(span)) {
  282. return (
  283. <SpanDetails>
  284. {organization.features.includes('profiling') ? (
  285. <GapSpanDetails event={event} span={span} resetCellMeasureCache={() => {}} />
  286. ) : (
  287. <InlineDocs
  288. orgSlug={organization.slug}
  289. platform={event.sdk?.name || ''}
  290. projectSlug={event?.projectSlug ?? ''}
  291. resetCellMeasureCache={() => {}}
  292. />
  293. )}
  294. </SpanDetails>
  295. );
  296. }
  297. const startTimestamp: number = span.start_timestamp;
  298. const endTimestamp: number = span.timestamp;
  299. const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
  300. getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
  301. const duration = endTimestamp - startTimestamp;
  302. const unknownKeys = Object.keys(span).filter(key => {
  303. return !isHiddenDataKey(key) && !rawSpanKeys.has(key as any);
  304. });
  305. const {sizeKeys, nonSizeKeys} = partitionSizes(span?.data ?? {});
  306. const allZeroSizes = SIZE_DATA_KEYS.map(key => sizeKeys[key]).every(
  307. value => value === 0
  308. );
  309. const timingKeys = getSpanSubTimings(span) ?? [];
  310. const parentTransaction = props.node.parent_transaction;
  311. const averageSpanSelfTime: number | undefined =
  312. span['span.averageResults']?.['avg(span.self_time)'];
  313. const averageSpanDuration: number | undefined =
  314. span['span.averageResults']?.['avg(span.duration)'];
  315. return (
  316. <Fragment>
  317. {renderOrphanSpanMessage()}
  318. {renderSpanErrorMessage()}
  319. {renderProfileMessage()}
  320. <SpanDetails>
  321. <table className="table key-value">
  322. <tbody>
  323. <Row title={t('Duration')}>
  324. <TraceDrawerComponents.Duration
  325. duration={duration}
  326. baseline={averageSpanDuration ? averageSpanDuration / 1000 : undefined}
  327. baseDescription={t(
  328. 'Average total time for this span group across the project associated with its parent transaction, over the last 24 hours'
  329. )}
  330. />
  331. </Row>
  332. {span.exclusive_time ? (
  333. <Row
  334. title={t('Self Time')}
  335. toolTipText={t(
  336. 'The time spent exclusively in this span, excluding the total duration of its children'
  337. )}
  338. >
  339. <TraceDrawerComponents.Duration
  340. ratio={span.exclusive_time / 1000 / duration}
  341. duration={span.exclusive_time / 1000}
  342. baseline={
  343. averageSpanSelfTime ? averageSpanSelfTime / 1000 : undefined
  344. }
  345. baseDescription={t(
  346. 'Average self time for this span group across the project associated with its parent transaction, over the last 24 hours'
  347. )}
  348. />
  349. </Row>
  350. ) : null}
  351. {parentTransaction ? (
  352. <Row title="Parent Transaction">
  353. <td className="value">
  354. <a href="#" onClick={() => props.onParentClick(parentTransaction)}>
  355. {getTraceTabTitle(parentTransaction)}
  356. </a>
  357. </td>
  358. </Row>
  359. ) : null}
  360. <Row
  361. title={
  362. isGapSpan(span) ? (
  363. <SpanIdTitle>Span ID</SpanIdTitle>
  364. ) : (
  365. <SpanIdTitle
  366. onClick={scrollToSpan(
  367. span.span_id,
  368. () => {},
  369. location,
  370. organization
  371. )}
  372. >
  373. Span ID
  374. </SpanIdTitle>
  375. )
  376. }
  377. extra={renderTraversalButton()}
  378. >
  379. {span.span_id}
  380. <CopyToClipboardButton
  381. borderless
  382. size="zero"
  383. iconSize="xs"
  384. text={span.span_id}
  385. />
  386. </Row>
  387. {profileId && project?.slug && (
  388. <Row
  389. title="Profile ID"
  390. extra={
  391. <TransactionToProfileButton
  392. size="xs"
  393. projectSlug={project.slug}
  394. event={event}
  395. query={{
  396. spanId: span.span_id,
  397. }}
  398. >
  399. {t('View Profile')}
  400. </TransactionToProfileButton>
  401. }
  402. >
  403. {profileId}
  404. </Row>
  405. )}
  406. <Row title={t('Status')}>{span.status || ''}</Row>
  407. <SpanHTTPInfo span={span} />
  408. <Row
  409. title={
  410. resolvedModule === ModuleName.DB && span.op?.startsWith('db')
  411. ? t('Database Query')
  412. : t('Description')
  413. }
  414. extra={renderSpanDetailActions()}
  415. >
  416. {resolvedModule === ModuleName.DB ? (
  417. <SpanDescriptionWrapper>
  418. <SpanDescription
  419. groupId={span.sentry_tags?.group ?? ''}
  420. op={span.op ?? ''}
  421. preliminaryDescription={span.description}
  422. />
  423. </SpanDescriptionWrapper>
  424. ) : (
  425. span.description
  426. )}
  427. </Row>
  428. <Row title={t('Date Range')}>
  429. {getDynamicText({
  430. fixed: 'Mar 16, 2020 9:10:12 AM UTC',
  431. value: (
  432. <Fragment>
  433. <DateTime date={startTimestamp * 1000} year seconds timeZone />
  434. {` (${startTimeWithLeadingZero})`}
  435. </Fragment>
  436. ),
  437. })}
  438. <br />
  439. {getDynamicText({
  440. fixed: 'Mar 16, 2020 9:10:13 AM UTC',
  441. value: (
  442. <Fragment>
  443. <DateTime date={endTimestamp * 1000} year seconds timeZone />
  444. {` (${endTimeWithLeadingZero})`}
  445. </Fragment>
  446. ),
  447. })}
  448. </Row>
  449. <Row title={t('Origin')}>
  450. {span.origin !== undefined ? String(span.origin) : null}
  451. </Row>
  452. <Row title="Parent Span ID">{span.parent_span_id || ''}</Row>
  453. {renderSpanChild()}
  454. <Row title={t('Same Process as Parent')}>
  455. {span.same_process_as_parent !== undefined
  456. ? String(span.same_process_as_parent)
  457. : null}
  458. </Row>
  459. <Row title={t('Span Group')}>
  460. {defined(span.hash) ? String(span.hash) : null}
  461. </Row>
  462. {timingKeys.map(timing => (
  463. <Row
  464. title={timing.name}
  465. key={timing.name}
  466. prefix={<RowTimingPrefix timing={timing} />}
  467. >
  468. {getPerformanceDuration(Number(timing.duration) * 1000)}
  469. </Row>
  470. ))}
  471. <Tags span={span} />
  472. {allZeroSizes && (
  473. <TextTr>
  474. The following sizes were not collected for security reasons. Check if
  475. the host serves the appropriate
  476. <ExternalLink href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin">
  477. <span className="val-string">Timing-Allow-Origin</span>
  478. </ExternalLink>
  479. header. You may have to enable this collection manually.
  480. </TextTr>
  481. )}
  482. {Object.entries(sizeKeys).map(([key, value]) => (
  483. <Row title={key} key={key}>
  484. <Fragment>
  485. <FileSize bytes={value} />
  486. {value >= 1024 && <span>{` (${value} B)`}</span>}
  487. </Fragment>
  488. </Row>
  489. ))}
  490. {Object.entries(nonSizeKeys).map(([key, value]) =>
  491. !isHiddenDataKey(key) ? (
  492. <Row title={key} key={key}>
  493. <GeneralSpanDetailsValue value={value} />
  494. </Row>
  495. ) : null
  496. )}
  497. {unknownKeys.map(key => {
  498. if (key === 'event' || key === 'childTransaction') {
  499. // dont render the entire JSON payload
  500. return null;
  501. }
  502. return (
  503. <Row title={key} key={key}>
  504. <GeneralSpanDetailsValue value={span[key]} />
  505. </Row>
  506. );
  507. })}
  508. </tbody>
  509. </table>
  510. {span._metrics_summary ? (
  511. <CustomMetricsEventData
  512. metricsSummary={span._metrics_summary}
  513. startTimestamp={span.start_timestamp}
  514. />
  515. ) : null}
  516. </SpanDetails>
  517. </Fragment>
  518. );
  519. }
  520. return (
  521. <SpanDetailContainer
  522. data-component="span-detail"
  523. onClick={event => {
  524. // prevent toggling the span detail
  525. event.stopPropagation();
  526. }}
  527. >
  528. {renderSpanDetails()}
  529. </SpanDetailContainer>
  530. );
  531. }
  532. function SpanHTTPInfo({span}: {span: RawSpanType}) {
  533. if (span.op === 'http.client' && span.description) {
  534. const [method, url] = span.description.split(' ');
  535. const parsedURL = safeURL(url);
  536. const queryString = qs.parse(parsedURL?.search ?? '');
  537. return parsedURL ? (
  538. <Fragment>
  539. <Row title={t('HTTP Method')}>{method}</Row>
  540. <Row title={t('URL')}>
  541. {parsedURL ? parsedURL?.origin + parsedURL?.pathname : 'failed to parse URL'}
  542. </Row>
  543. <Row title={t('Query')}>
  544. {parsedURL
  545. ? JSON.stringify(queryString, null, 2)
  546. : 'failed to parse query string'}
  547. </Row>
  548. </Fragment>
  549. ) : null;
  550. }
  551. return null;
  552. }
  553. function RowTimingPrefix({timing}: {timing: SubTimingInfo}) {
  554. return <OpsDot style={{backgroundColor: timing.color}} />;
  555. }
  556. const StyledDiscoverButton = styled(DiscoverButton)`
  557. position: absolute;
  558. top: ${space(0.75)};
  559. right: ${space(0.5)};
  560. `;
  561. const StyledButton = styled(Button)``;
  562. export const SpanDetailContainer = styled('div')`
  563. border-bottom: 1px solid ${p => p.theme.border};
  564. cursor: auto;
  565. `;
  566. export const SpanDetails = styled('div')`
  567. table.table.key-value td.key {
  568. max-width: 280px;
  569. }
  570. pre {
  571. overflow: hidden !important;
  572. }
  573. `;
  574. const ValueTd = styled('td')`
  575. position: relative;
  576. `;
  577. const StyledLoadingIndicator = styled(LoadingIndicator)`
  578. display: flex;
  579. align-items: center;
  580. height: ${space(2)};
  581. margin: 0;
  582. `;
  583. const StyledText = styled('p')`
  584. font-size: ${p => p.theme.fontSizeMedium};
  585. margin: ${space(2)} ${space(0)};
  586. `;
  587. function TextTr({children}) {
  588. return (
  589. <tr>
  590. <td className="key" />
  591. <ValueTd className="value">
  592. <StyledText>{children}</StyledText>
  593. </ValueTd>
  594. </tr>
  595. );
  596. }
  597. const SpanIdTitle = styled('a')`
  598. display: flex;
  599. color: ${p => p.theme.textColor};
  600. :hover {
  601. color: ${p => p.theme.textColor};
  602. }
  603. `;
  604. export function Row({
  605. title,
  606. keep,
  607. children,
  608. prefix,
  609. extra = null,
  610. toolTipText,
  611. }: {
  612. children: React.ReactNode;
  613. title: JSX.Element | string | null;
  614. extra?: React.ReactNode;
  615. keep?: boolean;
  616. prefix?: JSX.Element;
  617. toolTipText?: string;
  618. }) {
  619. if (!keep && !children) {
  620. return null;
  621. }
  622. return (
  623. <tr>
  624. <td className="key">
  625. <Flex>
  626. {prefix}
  627. {title}
  628. {toolTipText ? <StyledQuestionTooltip size="xs" title={toolTipText} /> : null}
  629. </Flex>
  630. </td>
  631. <ValueTd className="value">
  632. <ValueRow>
  633. <StyledPre>
  634. <span className="val-string">{children}</span>
  635. </StyledPre>
  636. <ButtonContainer>{extra}</ButtonContainer>
  637. </ValueRow>
  638. </ValueTd>
  639. </tr>
  640. );
  641. }
  642. export function Tags({span}: {span: RawSpanType}) {
  643. const tags: {[tag_name: string]: string} | undefined = span?.tags;
  644. if (!tags) {
  645. return null;
  646. }
  647. const keys = Object.keys(tags);
  648. if (keys.length <= 0) {
  649. return null;
  650. }
  651. return (
  652. <tr>
  653. <td className="key">Tags</td>
  654. <td className="value">
  655. <Pills style={{padding: '8px'}}>
  656. {keys.map((key, index) => (
  657. <Pill key={index} name={key} value={String(tags[key]) || ''} />
  658. ))}
  659. </Pills>
  660. </td>
  661. </tr>
  662. );
  663. }
  664. function generateSlug(result: TransactionResult): string {
  665. return generateEventSlug({
  666. id: result.id,
  667. 'project.name': result['project.name'],
  668. });
  669. }
  670. const Flex = styled('div')`
  671. display: flex;
  672. align-items: center;
  673. `;
  674. export const ButtonGroup = styled('div')`
  675. display: flex;
  676. flex-direction: column;
  677. gap: ${space(0.5)};
  678. `;
  679. export const ValueRow = styled('div')`
  680. display: grid;
  681. grid-template-columns: auto min-content;
  682. gap: ${space(1)};
  683. border-radius: 4px;
  684. background-color: ${p => p.theme.surface200};
  685. margin: 2px;
  686. `;
  687. const StyledPre = styled('pre')`
  688. margin: 0 !important;
  689. background-color: transparent !important;
  690. `;
  691. export const ButtonContainer = styled('div')`
  692. padding: 8px 10px;
  693. `;
  694. const StyledQuestionTooltip = styled(QuestionTooltip)`
  695. margin-left: ${space(0.5)};
  696. `;
  697. const SpanDescriptionWrapper = styled('div')`
  698. ${Frame} {
  699. border: none;
  700. }
  701. ${FrameContainer} {
  702. padding: ${space(2)} 0 0 0;
  703. margin-top: ${space(2)};
  704. }
  705. pre {
  706. padding: 0 !important;
  707. }
  708. `;
  709. export default NewTraceDetailsSpanDetail;