newTraceDetailsSpanDetails.tsx 22 KB

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