newTraceDetailsSpanDetails.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import {Fragment, useEffect, 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 {trackAnalytics} from 'sentry/utils/analytics';
  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 type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  29. import {safeURL} from 'sentry/utils/url/safeURL';
  30. import {useLocation} from 'sentry/utils/useLocation';
  31. import useProjects from 'sentry/utils/useProjects';
  32. import {CustomMetricsEventData} from 'sentry/views/ddm/customMetricsEventData';
  33. import {IssueList} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/issues/issues';
  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. openPanel: string | undefined;
  81. organization: Organization;
  82. span: RawSpanType;
  83. trace: Readonly<ParsedTraceType>;
  84. };
  85. function NewTraceDetailsSpanDetail(props: SpanDetailProps) {
  86. const location = useLocation();
  87. const profileId = props.event.contexts.profile?.profile_id || '';
  88. const issues = useMemo(() => {
  89. return [...props.node.errors, ...props.node.performance_issues];
  90. }, [props.node.errors, props.node.performance_issues]);
  91. const {projects} = useProjects();
  92. const project = projects.find(p => p.id === props.event.projectID);
  93. const resolvedModule: ModuleName = resolveSpanModule(
  94. props.span.sentry_tags?.op,
  95. props.span.sentry_tags?.category
  96. );
  97. useEffect(() => {
  98. // Run on mount.
  99. const {span, organization, event} = props;
  100. if (!('op' in span)) {
  101. return;
  102. }
  103. trackAnalytics('performance_views.event_details.open_span_details', {
  104. organization,
  105. operation: span.op ?? 'undefined',
  106. origin: span.origin ?? 'undefined',
  107. project_platform: event.platform ?? 'undefined',
  108. });
  109. // eslint-disable-next-line react-hooks/exhaustive-deps
  110. }, []);
  111. function renderTraversalButton(): React.ReactNode {
  112. if (!props.childTransactions) {
  113. // TODO: Amend size to use theme when we eventually refactor LoadingIndicator
  114. // 12px is consistent with theme.iconSizes['xs'] but theme returns a string.
  115. return (
  116. <StyledDiscoverButton size="xs" disabled>
  117. <StyledLoadingIndicator size={12} />
  118. </StyledDiscoverButton>
  119. );
  120. }
  121. if (props.childTransactions.length <= 0) {
  122. return null;
  123. }
  124. const {span, trace, event, organization} = props;
  125. assert(!isGapSpan(span));
  126. if (props.childTransactions.length === 1) {
  127. // Note: This is rendered by renderSpanChild() as a dedicated row
  128. return null;
  129. }
  130. const {start, end} = getTraceDateTimeRange({
  131. start: trace.traceStartTimestamp,
  132. end: trace.traceEndTimestamp,
  133. });
  134. const childrenEventView = EventView.fromSavedQuery({
  135. id: undefined,
  136. name: `Children from Span ID ${span.span_id}`,
  137. fields: [
  138. 'transaction',
  139. 'project',
  140. 'trace.span',
  141. 'transaction.duration',
  142. 'timestamp',
  143. ],
  144. orderby: '-timestamp',
  145. query: `event.type:transaction trace:${span.trace_id} trace.parent_span:${span.span_id}`,
  146. projects: organization.features.includes('global-views')
  147. ? [ALL_ACCESS_PROJECTS]
  148. : [Number(event.projectID)],
  149. version: 2,
  150. start,
  151. end,
  152. });
  153. return (
  154. <StyledDiscoverButton
  155. data-test-id="view-child-transactions"
  156. size="xs"
  157. to={childrenEventView.getResultsViewUrlTarget(organization.slug)}
  158. >
  159. {t('View Children')}
  160. </StyledDiscoverButton>
  161. );
  162. }
  163. function renderSpanChild(): React.ReactNode {
  164. const {childTransactions, organization} = props;
  165. if (!childTransactions || childTransactions.length !== 1) {
  166. return null;
  167. }
  168. const childTransaction = childTransactions[0];
  169. const transactionResult: TransactionResult = {
  170. 'project.name': childTransaction.project_slug,
  171. transaction: childTransaction.transaction,
  172. 'trace.span': childTransaction.span_id,
  173. id: childTransaction.event_id,
  174. };
  175. const eventSlug = generateSlug(transactionResult);
  176. const viewChildButton = (
  177. <SpanEntryContext.Consumer>
  178. {({getViewChildTransactionTarget}) => {
  179. const to = getViewChildTransactionTarget({
  180. ...transactionResult,
  181. eventSlug,
  182. });
  183. if (!to) {
  184. return null;
  185. }
  186. const target = transactionSummaryRouteWithQuery({
  187. orgSlug: organization.slug,
  188. transaction: transactionResult.transaction,
  189. query: omit(location.query, Object.values(PAGE_URL_PARAM)),
  190. projectID: String(childTransaction.project_id),
  191. });
  192. return (
  193. <ButtonGroup>
  194. <StyledButton data-test-id="view-child-transaction" size="xs" to={to}>
  195. {t('View Transaction')}
  196. </StyledButton>
  197. <StyledButton size="xs" to={target}>
  198. {t('View Summary')}
  199. </StyledButton>
  200. </ButtonGroup>
  201. );
  202. }}
  203. </SpanEntryContext.Consumer>
  204. );
  205. return (
  206. <Row title={t('Child Transaction')} extra={viewChildButton}>
  207. {`${transactionResult.transaction} (${transactionResult['project.name']})`}
  208. </Row>
  209. );
  210. }
  211. function renderSpanDetailActions() {
  212. const {span, organization, event} = props;
  213. if (isGapSpan(span) || !span.op || !span.hash) {
  214. return null;
  215. }
  216. const transactionName = event.title;
  217. return (
  218. <ButtonGroup>
  219. <SpanSummaryButton event={event} organization={organization} span={span} />
  220. <StyledButton
  221. size="xs"
  222. to={spanDetailsRouteWithQuery({
  223. orgSlug: organization.slug,
  224. transaction: transactionName,
  225. query: location.query,
  226. spanSlug: {op: span.op, group: span.hash},
  227. projectID: event.projectID,
  228. })}
  229. >
  230. {t('View Similar Spans')}
  231. </StyledButton>
  232. </ButtonGroup>
  233. );
  234. }
  235. function renderOrphanSpanMessage() {
  236. const {span} = props;
  237. if (!isOrphanSpan(span)) {
  238. return null;
  239. }
  240. return (
  241. <Alert type="info" showIcon system>
  242. {t(
  243. 'This is a span that has no parent span within this transaction. It has been attached to the transaction root span by default.'
  244. )}
  245. </Alert>
  246. );
  247. }
  248. function renderSpanErrorMessage() {
  249. const {span, organization, node} = props;
  250. const hasErrors = node.errors.length > 0 || node.performance_issues.length > 0;
  251. if (!hasErrors || isGapSpan(span)) {
  252. return null;
  253. }
  254. return <IssueList organization={organization} issues={issues} node={props.node} />;
  255. }
  256. function partitionSizes(data): {
  257. nonSizeKeys: {[key: string]: unknown};
  258. sizeKeys: {[key: string]: number};
  259. } {
  260. const sizeKeys = SIZE_DATA_KEYS.reduce((keys, key) => {
  261. if (data.hasOwnProperty(key) && defined(data[key])) {
  262. try {
  263. keys[key] = parseInt(data[key], 10);
  264. } catch (e) {
  265. keys[key] = data[key];
  266. }
  267. }
  268. return keys;
  269. }, {});
  270. const nonSizeKeys = {...data};
  271. SIZE_DATA_KEYS.forEach(key => delete nonSizeKeys[key]);
  272. return {
  273. sizeKeys,
  274. nonSizeKeys,
  275. };
  276. }
  277. function renderProfileMessage() {
  278. const {organization, span, event} = props;
  279. if (!organization.features.includes('profiling') || isGapSpan(span)) {
  280. return null;
  281. }
  282. return <SpanProfileDetails span={span} event={event} />;
  283. }
  284. function renderSpanDetails() {
  285. const {span, event, organization} = props;
  286. if (isGapSpan(span)) {
  287. return (
  288. <SpanDetails>
  289. {organization.features.includes('profiling') ? (
  290. <GapSpanDetails event={event} span={span} resetCellMeasureCache={() => {}} />
  291. ) : (
  292. <InlineDocs
  293. orgSlug={organization.slug}
  294. platform={event.sdk?.name || ''}
  295. projectSlug={event?.projectSlug ?? ''}
  296. resetCellMeasureCache={() => {}}
  297. />
  298. )}
  299. </SpanDetails>
  300. );
  301. }
  302. const startTimestamp: number = span.start_timestamp;
  303. const endTimestamp: number = span.timestamp;
  304. const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
  305. getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
  306. const duration = (endTimestamp - startTimestamp) * 1000;
  307. const durationString = `${Number(duration.toFixed(3)).toLocaleString()}ms`;
  308. const unknownKeys = Object.keys(span).filter(key => {
  309. return !isHiddenDataKey(key) && !rawSpanKeys.has(key as any);
  310. });
  311. const {sizeKeys, nonSizeKeys} = partitionSizes(span?.data ?? {});
  312. const allZeroSizes = SIZE_DATA_KEYS.map(key => sizeKeys[key]).every(
  313. value => value === 0
  314. );
  315. const timingKeys = getSpanSubTimings(span) ?? [];
  316. return (
  317. <Fragment>
  318. {renderOrphanSpanMessage()}
  319. {renderSpanErrorMessage()}
  320. {renderProfileMessage()}
  321. <SpanDetails>
  322. <table className="table key-value">
  323. <tbody>
  324. <Row
  325. title={
  326. isGapSpan(span) ? (
  327. <SpanIdTitle>Span ID</SpanIdTitle>
  328. ) : (
  329. <SpanIdTitle
  330. onClick={scrollToSpan(
  331. span.span_id,
  332. () => {},
  333. location,
  334. organization
  335. )}
  336. >
  337. Span ID
  338. </SpanIdTitle>
  339. )
  340. }
  341. extra={renderTraversalButton()}
  342. >
  343. {span.span_id}
  344. <CopyToClipboardButton
  345. borderless
  346. size="zero"
  347. iconSize="xs"
  348. text={`${window.location.href.replace(window.location.hash, '')}#span-${
  349. span.span_id
  350. }`}
  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. padding: ${space(2)};
  538. table.table.key-value td.key {
  539. max-width: 280px;
  540. }
  541. pre {
  542. overflow: hidden !important;
  543. }
  544. `;
  545. const ValueTd = styled('td')`
  546. position: relative;
  547. `;
  548. const StyledLoadingIndicator = styled(LoadingIndicator)`
  549. display: flex;
  550. align-items: center;
  551. height: ${space(2)};
  552. margin: 0;
  553. `;
  554. const StyledText = styled('p')`
  555. font-size: ${p => p.theme.fontSizeMedium};
  556. margin: ${space(2)} ${space(0)};
  557. `;
  558. function TextTr({children}) {
  559. return (
  560. <tr>
  561. <td className="key" />
  562. <ValueTd className="value">
  563. <StyledText>{children}</StyledText>
  564. </ValueTd>
  565. </tr>
  566. );
  567. }
  568. const SpanIdTitle = styled('a')`
  569. display: flex;
  570. color: ${p => p.theme.textColor};
  571. :hover {
  572. color: ${p => p.theme.textColor};
  573. }
  574. `;
  575. export function Row({
  576. title,
  577. keep,
  578. children,
  579. prefix,
  580. extra = null,
  581. }: {
  582. children: React.ReactNode;
  583. title: JSX.Element | string | null;
  584. extra?: React.ReactNode;
  585. keep?: boolean;
  586. prefix?: JSX.Element;
  587. }) {
  588. if (!keep && !children) {
  589. return null;
  590. }
  591. return (
  592. <tr>
  593. <td className="key">
  594. <Flex>
  595. {prefix}
  596. {title}
  597. </Flex>
  598. </td>
  599. <ValueTd className="value">
  600. <ValueRow>
  601. <StyledPre>
  602. <span className="val-string">{children}</span>
  603. </StyledPre>
  604. <ButtonContainer>{extra}</ButtonContainer>
  605. </ValueRow>
  606. </ValueTd>
  607. </tr>
  608. );
  609. }
  610. export function Tags({span}: {span: RawSpanType}) {
  611. const tags: {[tag_name: string]: string} | undefined = span?.tags;
  612. if (!tags) {
  613. return null;
  614. }
  615. const keys = Object.keys(tags);
  616. if (keys.length <= 0) {
  617. return null;
  618. }
  619. return (
  620. <tr>
  621. <td className="key">Tags</td>
  622. <td className="value">
  623. <Pills style={{padding: '8px'}}>
  624. {keys.map((key, index) => (
  625. <Pill key={index} name={key} value={String(tags[key]) || ''} />
  626. ))}
  627. </Pills>
  628. </td>
  629. </tr>
  630. );
  631. }
  632. function generateSlug(result: TransactionResult): string {
  633. return generateEventSlug({
  634. id: result.id,
  635. 'project.name': result['project.name'],
  636. });
  637. }
  638. const Flex = styled('div')`
  639. display: flex;
  640. align-items: center;
  641. `;
  642. const ButtonGroup = styled('div')`
  643. display: flex;
  644. flex-direction: column;
  645. gap: ${space(0.5)};
  646. `;
  647. const ValueRow = styled('div')`
  648. display: grid;
  649. grid-template-columns: auto min-content;
  650. gap: ${space(1)};
  651. border-radius: 4px;
  652. background-color: ${p => p.theme.surface200};
  653. margin: 2px;
  654. `;
  655. const StyledPre = styled('pre')`
  656. margin: 0 !important;
  657. background-color: transparent !important;
  658. `;
  659. const ButtonContainer = styled('div')`
  660. padding: 8px 10px;
  661. `;
  662. export default NewTraceDetailsSpanDetail;