Browse Source

feat(new-trace-txn-details): Using new tags/context ui for drawer sections (#69911)

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdkhan14 10 months ago
parent
commit
f60e13eedb

+ 1 - 0
static/app/components/events/eventTags/util.tsx

@@ -156,6 +156,7 @@ export function useHasNewTagsUI() {
   const organization = useOrganization();
   return (
     location.query.tagsTree === '1' ||
+    location.query.traceView === '1' ||
     organization.features.includes('event-tags-tree-ui')
   );
 }

+ 1 - 1
static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx

@@ -506,7 +506,7 @@ const makeRow = (
   value: KeyValueListDataItem['value'],
   actionButton?: ReactNode
 ): KeyValueListDataItem => {
-  const itemKey = kebabCase(subject);
+  const itemKey = kebabCase(subject ?? '');
 
   return {
     key: itemKey,

+ 16 - 0
static/app/views/performance/newTraceDetails/index.tsx

@@ -14,6 +14,7 @@ import * as Sentry from '@sentry/react';
 import * as qs from 'query-string';
 
 import {Button} from 'sentry/components/button';
+import {useHasNewTagsUI} from 'sentry/components/events/eventTags/util';
 import useFeedbackWidget from 'sentry/components/feedback/widget/useFeedbackWidget';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import NoProjectMessage from 'sentry/components/noProjectMessage';
@@ -91,6 +92,7 @@ function logTraceType(type: TraceType, organization: Organization) {
 export function TraceView() {
   const params = useParams<{traceSlug?: string}>();
   const organization = useOrganization();
+  const hasNewTagsUI = useHasNewTagsUI();
 
   const traceSlug = useMemo(() => {
     const slug = params.traceSlug?.trim() ?? '';
@@ -105,6 +107,20 @@ export function TraceView() {
     return slug;
   }, [params.traceSlug]);
 
+  useLayoutEffect(() => {
+    if (hasNewTagsUI) {
+      return;
+    }
+
+    // Enables the new trace tags/contexts ui for the trace view
+    const queryString = qs.parse(window.location.search);
+    queryString.traceView = '1';
+    browserHistory.replace({
+      pathname: window.location.pathname,
+      query: queryString,
+    });
+  }, [traceSlug, hasNewTagsUI]);
+
   useEffect(() => {
     trackAnalytics('performance_views.trace_view_v1_page_load', {
       organization,

+ 3 - 3
static/app/views/performance/newTraceDetails/trace.spec.tsx

@@ -120,7 +120,7 @@ function mockTransactionDetailsResponse(id: string, resp?: Partial<ResponseType>
     url: `/organizations/org-slug/events/project_slug:${id}/`,
     method: 'GET',
     asyncDelay: 1,
-    ...(resp ?? {}),
+    ...(resp ?? {body: DetailedEventsFixture()[0]}),
   });
 }
 
@@ -129,7 +129,7 @@ function mockTraceRootEvent(id: string, resp?: Partial<ResponseType>) {
     url: `/organizations/org-slug/events/project_slug:${id}/`,
     method: 'GET',
     asyncDelay: 1,
-    ...(resp ?? {}),
+    ...(resp ?? {body: DetailedEventsFixture()[0]}),
   });
 }
 
@@ -326,7 +326,7 @@ async function searchTestSetup() {
   mockTraceMetaResponse();
   mockTraceRootFacets();
   mockTraceRootEvent('0', {body: DetailedEventsFixture()[0]});
-  mockTraceEventDetails();
+  mockTraceEventDetails({body: DetailedEventsFixture()[0]});
 
   const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
   const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');

+ 23 - 7
static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx

@@ -7,6 +7,7 @@ import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import {Tooltip} from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import type {Organization, Project} from 'sentry/types';
+import {useLocation} from 'sentry/utils/useLocation';
 import useProjects from 'sentry/utils/useProjects';
 import {CustomMetricsEventData} from 'sentry/views/metrics/customMetricsEventData';
 import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
@@ -21,7 +22,11 @@ import {TraceDrawerComponents} from '.././styles';
 import {IssueList} from '../issues/issues';
 
 import Alerts from './sections/alerts';
-import SpanNodeDetailTable from './sections/table/index';
+import {SpanDescription} from './sections/description';
+import {GeneralInfo} from './sections/generalInfo';
+import {SpanHTTPInfo} from './sections/http';
+import {SpanKeys} from './sections/keys';
+import {Tags} from './sections/tags';
 
 function SpanNodeDetailHeader({
   node,
@@ -70,6 +75,7 @@ export function SpanNodeDetails({
   onTabScrollToNode,
   onParentClick,
 }: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.Span>>) {
+  const location = useLocation();
   const {projects} = useProjects();
   const {event} = node.value;
   const issues = useMemo(() => {
@@ -103,12 +109,22 @@ export function SpanNodeDetails({
                 {issues.length > 0 ? (
                   <IssueList organization={organization} issues={issues} node={node} />
                 ) : null}
-                <SpanNodeDetailTable
-                  node={node}
-                  openPanel="open"
-                  organization={organization}
-                  onParentClick={onParentClick}
-                />
+                <div>
+                  <SpanDescription
+                    node={node}
+                    organization={organization}
+                    location={location}
+                  />
+                  <GeneralInfo
+                    node={node}
+                    organization={organization}
+                    location={location}
+                    onParentClick={onParentClick}
+                  />
+                  <SpanHTTPInfo span={node.value} />
+                  <Tags span={node.value} />
+                  <SpanKeys node={node} />
+                </div>
                 {node.value._metrics_summary ? (
                   <CustomMetricsEventData
                     projectId={project?.id || ''}

+ 80 - 72
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/table/rows/ancestry.tsx → static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/ancestry.tsx

@@ -12,6 +12,7 @@ import {
   isGapSpan,
   scrollToSpan,
 } from 'sentry/components/events/interfaces/spans/utils';
+import Link from 'sentry/components/links/link';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {ALL_ACCESS_PROJECTS, PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
@@ -28,8 +29,7 @@ import type {
 import {getTraceTabTitle} from 'sentry/views/performance/newTraceDetails/traceState/traceTabs';
 import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
 
-import {TraceDrawerComponents} from '../../../../styles';
-import {ButtonGroup} from '..';
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '../../styles';
 
 type TransactionResult = {
   id: string;
@@ -65,7 +65,7 @@ function SpanChild({
     'project.name': transactionResult['project.name'],
   });
 
-  const viewChildButton = (
+  const value = (
     <SpanEntryContext.Consumer>
       {({getViewChildTransactionTarget}) => {
         const to = getViewChildTransactionTarget({
@@ -74,7 +74,7 @@ function SpanChild({
         });
 
         if (!to) {
-          return null;
+          return `${transactionResult.transaction} (${transactionResult['project.name']})`;
         }
 
         const target = transactionSummaryRouteWithQuery({
@@ -85,27 +85,20 @@ function SpanChild({
         });
 
         return (
-          <ButtonGroup>
-            <Button data-test-id="view-child-transaction" size="xs" to={to}>
-              {t('View Transaction')}
-            </Button>
+          <SpanChildValueWrapper>
+            <Link data-test-id="view-child-transaction" to={to}>
+              {`${transactionResult.transaction} (${transactionResult['project.name']})`}
+            </Link>
             <Button size="xs" to={target}>
               {t('View Summary')}
             </Button>
-          </ButtonGroup>
+          </SpanChildValueWrapper>
         );
       }}
     </SpanEntryContext.Consumer>
   );
 
-  return (
-    <TraceDrawerComponents.TableRow
-      title={t('Child Transaction')}
-      extra={viewChildButton}
-    >
-      {`${transactionResult.transaction} (${transactionResult['project.name']})`}
-    </TraceDrawerComponents.TableRow>
-  );
+  return value;
 }
 
 function SpanChildrenTraversalButton({
@@ -166,7 +159,7 @@ function SpanChildrenTraversalButton({
   );
 }
 
-export function AncestryAndGrouping({
+export function getSpanAncestryAndGroupingItems({
   node,
   onParentClick,
   location,
@@ -176,56 +169,74 @@ export function AncestryAndGrouping({
   node: TraceTreeNode<TraceTree.Span>;
   onParentClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
   organization: Organization;
-}) {
+}): SectionCardKeyValueList {
   const parentTransaction = node.parent_transaction;
   const span = node.value;
-  return (
-    <Fragment>
-      {parentTransaction ? (
-        <TraceDrawerComponents.TableRow title="Parent Transaction">
-          <td className="value">
-            <a href="#" onClick={() => onParentClick(parentTransaction)}>
-              {getTraceTabTitle(parentTransaction)}
-            </a>
-          </td>
-        </TraceDrawerComponents.TableRow>
-      ) : null}
-
-      <TraceDrawerComponents.TableRow
-        title={
-          isGapSpan(span) ? (
-            <SpanIdTitle>Span ID</SpanIdTitle>
-          ) : (
-            <SpanIdTitle
-              onClick={scrollToSpan(span.span_id, () => {}, location, organization)}
-            >
-              Span ID
-            </SpanIdTitle>
-          )
-        }
-        extra={<SpanChildrenTraversalButton node={node} organization={organization} />}
-      >
+  const items: SectionCardKeyValueList = [];
+
+  if (parentTransaction) {
+    items.push({
+      key: 'parent_transaction',
+      value: (
+        <a href="#" onClick={() => onParentClick(parentTransaction)}>
+          {getTraceTabTitle(parentTransaction)}
+        </a>
+      ),
+      subject: t('Parent Transaction'),
+    });
+  }
+
+  items.push({
+    key: 'span_id',
+    value: (
+      <Fragment>
         {span.span_id}
         <CopyToClipboardButton borderless size="zero" iconSize="xs" text={span.span_id} />
-      </TraceDrawerComponents.TableRow>
-      <TraceDrawerComponents.TableRow title={t('Origin')}>
-        {span.origin !== undefined ? String(span.origin) : null}
-      </TraceDrawerComponents.TableRow>
-
-      <TraceDrawerComponents.TableRow title="Parent Span ID">
-        {span.parent_span_id || ''}
-      </TraceDrawerComponents.TableRow>
-      <SpanChild node={node} organization={organization} location={location} />
-      <TraceDrawerComponents.TableRow title={t('Same Process as Parent')}>
-        {span.same_process_as_parent !== undefined
-          ? String(span.same_process_as_parent)
-          : null}
-      </TraceDrawerComponents.TableRow>
-      <TraceDrawerComponents.TableRow title={t('Span Group')}>
-        {defined(span.hash) ? String(span.hash) : null}
-      </TraceDrawerComponents.TableRow>
-    </Fragment>
-  );
+      </Fragment>
+    ),
+    subject: (
+      <TraceDrawerComponents.FlexBox style={{gap: '5px'}}>
+        <span onClick={scrollToSpan(span.span_id, () => {}, location, organization)}>
+          Span ID
+        </span>
+        <SpanChildrenTraversalButton node={node} organization={organization} />
+      </TraceDrawerComponents.FlexBox>
+    ),
+  });
+
+  items.push({
+    key: 'origin',
+    value: span.origin !== undefined ? String(span.origin) : null,
+    subject: t('Origin'),
+  });
+
+  items.push({
+    key: 'parent_span_id',
+    value: span.parent_span_id || '',
+    subject: t('Parent Span ID'),
+  });
+
+  items.push({
+    key: 'transaction_name',
+    value: <SpanChild node={node} organization={organization} location={location} />,
+    subject: t('Child Transaction'),
+  });
+
+  if (span.same_process_as_parent) {
+    items.push({
+      key: 'same_process_as_parent',
+      value: String(span.same_process_as_parent),
+      subject: t('Same Process as Parent'),
+    });
+  }
+
+  items.push({
+    key: 'same_group',
+    value: defined(span.hash) ? String(span.hash) : null,
+    subject: t('Span Group'),
+  });
+
+  return items;
 }
 
 const StyledDiscoverButton = styled(DiscoverButton)`
@@ -234,17 +245,14 @@ const StyledDiscoverButton = styled(DiscoverButton)`
   right: ${space(0.5)};
 `;
 
-const SpanIdTitle = styled('a')`
-  display: flex;
-  color: ${p => p.theme.textColor};
-  :hover {
-    color: ${p => p.theme.textColor};
-  }
-`;
-
 const StyledLoadingIndicator = styled(LoadingIndicator)`
   display: flex;
   align-items: center;
   height: ${space(2)};
   margin: 0;
 `;
+
+const SpanChildValueWrapper = styled(TraceDrawerComponents.FlexBox)`
+  justify-content: space-between;
+  gap: ${space(0.5)};
+`;

+ 117 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/description.tsx

@@ -0,0 +1,117 @@
+import styled from '@emotion/styled';
+import type {Location} from 'history';
+
+import {Button} from 'sentry/components/button';
+import SpanSummaryButton from 'sentry/components/events/interfaces/spans/spanSummaryButton';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {Organization} from 'sentry/types';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
+import {
+  Frame,
+  SpanDescription as DBQueryDescription,
+} from 'sentry/views/starfish/components/spanDescription';
+import {ModuleName} from 'sentry/views/starfish/types';
+import {resolveSpanModule} from 'sentry/views/starfish/utils/resolveSpanModule';
+
+import {TraceDrawerComponents} from '../../styles';
+
+export function SpanDescription({
+  node,
+  organization,
+  location,
+}: {
+  location: Location;
+  node: TraceTreeNode<TraceTree.Span>;
+  organization: Organization;
+}) {
+  const span = node.value;
+  const {event} = span;
+  const resolvedModule: ModuleName = resolveSpanModule(
+    span.sentry_tags?.op,
+    span.sentry_tags?.category
+  );
+
+  if (![ModuleName.DB, ModuleName.RESOURCE].includes(resolvedModule)) {
+    return null;
+  }
+
+  const actions =
+    !span.op || !span.hash ? null : (
+      <ButtonGroup>
+        <SpanSummaryButton event={event} organization={organization} span={span} />
+        <Button
+          size="xs"
+          to={spanDetailsRouteWithQuery({
+            orgSlug: organization.slug,
+            transaction: event.title,
+            query: location.query,
+            spanSlug: {op: span.op, group: span.hash},
+            projectID: event.projectID,
+          })}
+        >
+          {t('View Similar Spans')}
+        </Button>
+      </ButtonGroup>
+    );
+
+  const value =
+    resolvedModule === ModuleName.DB ? (
+      <SpanDescriptionWrapper>
+        <DBQueryDescription
+          groupId={span.sentry_tags?.group ?? ''}
+          op={span.op ?? ''}
+          preliminaryDescription={span.description}
+        />
+      </SpanDescriptionWrapper>
+    ) : (
+      span.description
+    );
+
+  const title =
+    resolvedModule === ModuleName.DB && span.op?.startsWith('db')
+      ? t('Database Query')
+      : t('Resource');
+
+  return (
+    <TraceDrawerComponents.SectionCard
+      items={[
+        {
+          key: 'description',
+          subject: null,
+          value,
+        },
+      ]}
+      title={
+        <TitleContainer>
+          {title}
+          {actions}
+        </TitleContainer>
+      }
+    />
+  );
+}
+
+const TitleContainer = styled('div')`
+  display: flex;
+  align-items: center;
+  margin-bottom: ${space(0.5)};
+  justify-content: space-between;
+`;
+
+const SpanDescriptionWrapper = styled('div')`
+  ${Frame} {
+    border: none;
+  }
+`;
+
+const ButtonGroup = styled('div')`
+  display: flex;
+  gap: ${space(0.5)};
+  flex-wrap: wrap;
+  justify-content: flex-end;
+`;

+ 136 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/generalInfo.tsx

@@ -0,0 +1,136 @@
+import type {Location} from 'history';
+
+import Link from 'sentry/components/links/link';
+import QuestionTooltip from 'sentry/components/questionTooltip';
+import {t} from 'sentry/locale';
+import type {Organization} from 'sentry/types';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
+import {ModuleName} from 'sentry/views/starfish/types';
+import {resolveSpanModule} from 'sentry/views/starfish/utils/resolveSpanModule';
+
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '../../styles';
+
+import {getSpanAncestryAndGroupingItems} from './ancestry';
+
+type GeneralnfoProps = {
+  location: Location;
+  node: TraceTreeNode<TraceTree.Span>;
+  onParentClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
+  organization: Organization;
+};
+
+function SpanDuration({node}: {node: TraceTreeNode<TraceTree.Span>}) {
+  const span = node.value;
+  const startTimestamp: number = span.start_timestamp;
+  const endTimestamp: number = span.timestamp;
+  const duration = endTimestamp - startTimestamp;
+  const averageSpanDuration: number | undefined =
+    span['span.averageResults']?.['avg(span.duration)'];
+
+  return (
+    <TraceDrawerComponents.Duration
+      duration={duration}
+      baseline={averageSpanDuration ? averageSpanDuration / 1000 : undefined}
+      baseDescription={t(
+        'Average total time for this span group across the project associated with its parent transaction, over the last 24 hours'
+      )}
+    />
+  );
+}
+
+function SpanSelfTime({node}: {node: TraceTreeNode<TraceTree.Span>}) {
+  const span = node.value;
+  const startTimestamp: number = span.start_timestamp;
+  const endTimestamp: number = span.timestamp;
+  const duration = endTimestamp - startTimestamp;
+  const averageSpanSelfTime: number | undefined =
+    span['span.averageResults']?.['avg(span.self_time)'];
+
+  return span.exclusive_time ? (
+    <TraceDrawerComponents.Duration
+      ratio={span.exclusive_time / 1000 / duration}
+      duration={span.exclusive_time / 1000}
+      baseline={averageSpanSelfTime ? averageSpanSelfTime / 1000 : undefined}
+      baseDescription={t(
+        'Average self time for this span group across the project associated with its parent transaction, over the last 24 hours'
+      )}
+    />
+  ) : null;
+}
+
+export function GeneralInfo(props: GeneralnfoProps) {
+  let items: SectionCardKeyValueList = [];
+
+  const span = props.node.value;
+  const {event} = span;
+  const resolvedModule: ModuleName = resolveSpanModule(
+    span.sentry_tags?.op,
+    span.sentry_tags?.category
+  );
+
+  if (![ModuleName.DB, ModuleName.RESOURCE].includes(resolvedModule)) {
+    items.push({
+      key: 'description',
+      subject: t('Description'),
+      value:
+        span.op && span.hash ? (
+          <Link
+            to={spanDetailsRouteWithQuery({
+              orgSlug: props.organization.slug,
+              transaction: event.title,
+              query: props.location.query,
+              spanSlug: {op: span.op, group: span.hash},
+              projectID: event.projectID,
+            })}
+          >
+            {span.description}
+          </Link>
+        ) : (
+          span.description
+        ),
+    });
+  }
+
+  items.push({
+    key: 'duration',
+    subject: t('Duration'),
+    value: <SpanDuration node={props.node} />,
+  });
+
+  if (props.node.value.exclusive_time) {
+    items.push({
+      key: 'self_time',
+      subject: (
+        <TraceDrawerComponents.FlexBox style={{gap: '5px'}}>
+          {t('Self Time')}
+          <QuestionTooltip
+            title={t('Applicable to the children of this event only')}
+            size="xs"
+          />
+        </TraceDrawerComponents.FlexBox>
+      ),
+      value: <SpanSelfTime node={props.node} />,
+    });
+  }
+
+  const ancestryAndGroupingItems = getSpanAncestryAndGroupingItems({
+    node: props.node,
+    onParentClick: props.onParentClick,
+    location: props.location,
+    organization: props.organization,
+  });
+
+  items = [...items, ...ancestryAndGroupingItems];
+
+  return (
+    <TraceDrawerComponents.SectionCard
+      disableTruncate
+      items={items}
+      title={t('General')}
+    />
+  );
+}

+ 53 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/http.tsx

@@ -0,0 +1,53 @@
+import qs from 'qs';
+
+import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
+import {t} from 'sentry/locale';
+import {safeURL} from 'sentry/utils/url/safeURL';
+
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '../../styles';
+
+export function SpanHTTPInfo({span}: {span: RawSpanType}) {
+  if (span.op === 'http.client' && span.description) {
+    const [method, url] = span.description.split(' ');
+
+    const parsedURL = safeURL(url);
+    const queryString = qs.parse(parsedURL?.search ?? '');
+
+    if (!parsedURL) {
+      return null;
+    }
+
+    const items: SectionCardKeyValueList = [
+      {
+        subject: t('Status'),
+        value: span.status || '',
+        key: 'status',
+      },
+      {
+        subject: t('HTTP Method'),
+        value: method,
+        key: 'method',
+      },
+      {
+        subject: t('URL'),
+        value: parsedURL
+          ? parsedURL?.origin + parsedURL?.pathname
+          : 'failed to parse URL',
+        key: 'url',
+      },
+      {
+        subject: t('Query'),
+        value: parsedURL
+          ? JSON.stringify(queryString, null, 2)
+          : `failed to parse query string from ${url}`,
+        key: 'query',
+      },
+    ];
+
+    return parsedURL ? (
+      <TraceDrawerComponents.SectionCard items={items} title={t('Http')} />
+    ) : null;
+  }
+
+  return null;
+}

+ 138 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/span/sections/keys.tsx

@@ -0,0 +1,138 @@
+import {Fragment} from 'react';
+
+import {
+  rawSpanKeys,
+  type RawSpanType,
+} from 'sentry/components/events/interfaces/spans/types';
+import {
+  getSpanSubTimings,
+  isHiddenDataKey,
+  type SubTimingInfo,
+} from 'sentry/components/events/interfaces/spans/utils';
+import {OpsDot} from 'sentry/components/events/opsBreakdown';
+import FileSize from 'sentry/components/fileSize';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {KeyValueListDataItem} from 'sentry/types';
+import {defined} from 'sentry/utils';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
+
+import {type SectionCardKeyValueList, TraceDrawerComponents} from '../../styles';
+
+const SIZE_DATA_KEYS = [
+  'Encoded Body Size',
+  'Decoded Body Size',
+  'Transfer Size',
+  'http.request_content_length',
+  'http.response_content_length',
+  'http.decoded_response_content_length',
+  'http.response_transfer_size',
+];
+
+function partitionSizes(data: RawSpanType['data']): {
+  nonSizeKeys: {[key: string]: unknown};
+  sizeKeys: {[key: string]: number};
+} {
+  const sizeKeys = SIZE_DATA_KEYS.reduce((keys, key) => {
+    if (data.hasOwnProperty(key) && defined(data[key])) {
+      try {
+        keys[key] = parseFloat(data[key]);
+      } catch (e) {
+        keys[key] = data[key];
+      }
+    }
+    return keys;
+  }, {});
+
+  const nonSizeKeys = {...data};
+  SIZE_DATA_KEYS.forEach(key => delete nonSizeKeys[key]);
+
+  return {
+    sizeKeys,
+    nonSizeKeys,
+  };
+}
+
+export function SpanKeys({node}: {node: TraceTreeNode<TraceTree.Span>}) {
+  const span = node.value;
+  const {sizeKeys, nonSizeKeys} = partitionSizes(span?.data ?? {});
+  const allZeroSizes = SIZE_DATA_KEYS.map(key => sizeKeys[key]).every(
+    value => value === 0
+  );
+  const unknownKeys = Object.keys(span).filter(key => {
+    return !isHiddenDataKey(key) && !rawSpanKeys.has(key as any);
+  });
+  const timingKeys = getSpanSubTimings(span) ?? [];
+
+  const items: SectionCardKeyValueList = [];
+
+  if (allZeroSizes) {
+    items.push({
+      key: 'all_zeros_text',
+      subject: null,
+      value: tct(
+        ' The following sizes were not collected for security reasons. Check if the host serves the appropriate [link] header. You may have to enable this collection manually.',
+        {
+          link: (
+            <ExternalLink href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin">
+              <span className="val-string">Timing-Allow-Origin</span>
+            </ExternalLink>
+          ),
+        }
+      ),
+    });
+  }
+  Object.entries(sizeKeys).forEach(([key, value]) => {
+    items.push({
+      key: key,
+      subject: key,
+      value: (
+        <Fragment>
+          <FileSize bytes={value} />
+          {value >= 1024 && <span>{` (${value} B)`}</span>}
+        </Fragment>
+      ),
+    });
+  });
+  Object.entries(nonSizeKeys).forEach(([key, value]) => {
+    if (!isHiddenDataKey(key)) {
+      items.push({
+        key: key,
+        subject: key,
+        value: value as KeyValueListDataItem['value'],
+      });
+    }
+  });
+  unknownKeys.forEach(key => {
+    if (key !== 'event' && key !== 'childTransactions') {
+      items.push({
+        key: key,
+        subject: key,
+        value: span[key],
+      });
+    }
+  });
+  timingKeys.forEach(timing => {
+    items.push({
+      key: timing.name,
+      subject: (
+        <TraceDrawerComponents.FlexBox style={{gap: space(0.5)}}>
+          <RowTimingPrefix timing={timing} />
+          {timing.name}
+        </TraceDrawerComponents.FlexBox>
+      ),
+      value: getPerformanceDuration(Number(timing.duration) * 1000),
+    });
+  });
+
+  return <TraceDrawerComponents.SectionCard items={items} title={t('Additional Data')} />;
+}
+
+function RowTimingPrefix({timing}: {timing: SubTimingInfo}) {
+  return <OpsDot style={{backgroundColor: timing.color}} />;
+}

Some files were not shown because too many files changed in this diff