Browse Source

feat(new-trace): Cleaning up transaction node detail code. (#69213)

Restructured from just having detail/transaction.tsx to:
- detail/transaction/
- - index.tsx
- - sectons
- - - example: replayPreview.tsx

Using new event context ui.

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

+ 1 - 1
static/app/components/events/eventEntries.tsx

@@ -149,7 +149,7 @@ function EventEntries({
 // Because replays are not an interface, we need to manually insert the replay section
 // into the array of entries. The long-term solution here is to move the ordering
 // logic to this component, similar to how GroupEventDetailsContent works.
-function partitionEntriesForReplay(entries: Entry[]) {
+export function partitionEntriesForReplay(entries: Entry[]) {
   let replayIndex = 0;
 
   for (const [i, entry] of entries.entries()) {

+ 72 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/styles.tsx

@@ -4,6 +4,7 @@ import * as qs from 'query-string';
 
 import {Button as CommonButton, LinkButton} from 'sentry/components/button';
 import {DataSection} from 'sentry/components/events/styles';
+import type {LazyRenderProps} from 'sentry/components/lazyRender';
 import {Tooltip} from 'sentry/components/tooltip';
 import {t, tct} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
@@ -247,6 +248,47 @@ function Duration(props: DurationProps) {
   );
 }
 
+function TableRow({
+  title,
+  keep,
+  children,
+  prefix,
+  extra = null,
+}: {
+  children: React.ReactNode;
+  title: JSX.Element | string | null;
+  extra?: React.ReactNode;
+  keep?: boolean;
+  prefix?: JSX.Element;
+}) {
+  if (!keep && !children) {
+    return null;
+  }
+
+  return (
+    <tr>
+      <td className="key">
+        <Flex>
+          {prefix}
+          {title}
+        </Flex>
+      </td>
+      <ValueTd className="value">
+        <ValueRow>
+          <StyledPre>
+            <span className="val-string">{children}</span>
+          </StyledPre>
+          <ButtonContainer>{extra}</ButtonContainer>
+        </ValueRow>
+      </ValueTd>
+    </tr>
+  );
+}
+
+const LAZY_RENDER_PROPS: Partial<LazyRenderProps> = {
+  observerOptions: {rootMargin: '50px'},
+};
+
 const DurationContainer = styled('span')`
   font-weight: bold;
   margin-right: ${space(1)};
@@ -256,6 +298,34 @@ const Comparison = styled('span')<{status: 'faster' | 'slower' | 'equal'}>`
   color: ${p => p.theme[DURATION_COMPARISON_STATUS_COLORS[p.status].normal]};
 `;
 
+const Flex = styled('div')`
+  display: flex;
+  align-items: center;
+`;
+
+const ValueRow = styled('div')`
+  display: grid;
+  grid-template-columns: auto min-content;
+  gap: ${space(1)};
+
+  border-radius: 4px;
+  background-color: ${p => p.theme.surface200};
+  margin: 2px;
+`;
+
+const StyledPre = styled('pre')`
+  margin: 0 !important;
+  background-color: transparent !important;
+`;
+
+const ButtonContainer = styled('div')`
+  padding: 8px 10px;
+`;
+
+const ValueTd = styled('td')`
+  position: relative;
+`;
+
 const TraceDrawerComponents = {
   DetailContainer,
   FlexBox,
@@ -271,6 +341,8 @@ const TraceDrawerComponents = {
   Button,
   TitleText,
   Duration,
+  TableRow,
+  LAZY_RENDER_PROPS,
 };
 
 export {TraceDrawerComponents};

+ 196 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/index.tsx

@@ -0,0 +1,196 @@
+import {useMemo} from 'react';
+
+import {Button} from 'sentry/components/button';
+import {EventContexts} from 'sentry/components/events/contexts';
+import {EventAttachments} from 'sentry/components/events/eventAttachments';
+import {EventEvidence} from 'sentry/components/events/eventEvidence';
+import {EventExtraData} from 'sentry/components/events/eventExtraData';
+import {EventSdk} from 'sentry/components/events/eventSdk';
+import {EventViewHierarchy} from 'sentry/components/events/eventViewHierarchy';
+import {EventRRWebIntegration} from 'sentry/components/events/rrwebIntegration';
+import FileSize from 'sentry/components/fileSize';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import type {LazyRenderProps} from 'sentry/components/lazyRender';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconOpen} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import type {EventTransaction, 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 {useTransaction} from 'sentry/views/performance/newTraceDetails/traceApi/useTransaction';
+import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+
+import {IssueList} from '../issues/issues';
+import {TraceDrawerComponents} from '../styles';
+
+import {BreadCrumbs} from './sections/breadCrumbs';
+import {Entries} from './sections/entries';
+import ReplayPreview from './sections/replayPreview';
+import {Table} from './sections/table';
+import {EventTags} from './sections/tags';
+
+export const LAZY_RENDER_PROPS: Partial<LazyRenderProps> = {
+  observerOptions: {rootMargin: '50px'},
+};
+
+type TransactionNodeDetailHeaderProps = {
+  event: EventTransaction;
+  node: TraceTreeNode<TraceTree.Transaction>;
+  onTabScrollToNode: (node: TraceTreeNode<TraceTree.Transaction>) => void;
+  organization: Organization;
+  project: Project | undefined;
+};
+
+function TransactionNodeDetailHeader({
+  node,
+  organization,
+  project,
+  onTabScrollToNode,
+  event,
+}: TransactionNodeDetailHeaderProps) {
+  return (
+    <TraceDrawerComponents.HeaderContainer>
+      <TraceDrawerComponents.Title>
+        <Tooltip title={node.value.project_slug}>
+          <ProjectBadge
+            project={project ? project : {slug: node.value.project_slug}}
+            avatarSize={30}
+            hideName
+          />
+        </Tooltip>
+        <TraceDrawerComponents.TitleText>
+          <div>{t('transaction')}</div>
+          <TraceDrawerComponents.TitleOp>
+            {' '}
+            {node.value['transaction.op'] + ' - ' + node.value.transaction}
+          </TraceDrawerComponents.TitleOp>
+        </TraceDrawerComponents.TitleText>
+      </TraceDrawerComponents.Title>
+
+      <TraceDrawerComponents.Actions>
+        <Button size="xs" onClick={_e => onTabScrollToNode(node)}>
+          {t('Show in view')}
+        </Button>
+        <TraceDrawerComponents.EventDetailsLink node={node} organization={organization} />
+        <Button
+          size="xs"
+          icon={<IconOpen />}
+          href={`/api/0/projects/${organization.slug}/${node.value.project_slug}/events/${node.value.event_id}/json/`}
+          external
+        >
+          {t('JSON')} (<FileSize bytes={event?.size} />)
+        </Button>
+      </TraceDrawerComponents.Actions>
+    </TraceDrawerComponents.HeaderContainer>
+  );
+}
+
+export function TransactionNodeDetails({
+  node,
+  organization,
+  onTabScrollToNode,
+  onParentClick,
+}: TraceTreeNodeDetailsProps<TraceTreeNode<TraceTree.Transaction>>) {
+  const location = useLocation();
+  const {projects} = useProjects();
+  const issues = useMemo(() => {
+    return [...node.errors, ...node.performance_issues];
+  }, [node.errors, node.performance_issues]);
+
+  const {
+    data: event,
+    isError,
+    isLoading,
+  } = useTransaction({
+    node,
+    organization,
+  });
+
+  if (isLoading) {
+    return <LoadingIndicator />;
+  }
+
+  if (isError) {
+    return <LoadingError message={t('Failed to fetch transaction details')} />;
+  }
+
+  const project = projects.find(proj => proj.slug === event?.projectSlug);
+
+  return (
+    <TraceDrawerComponents.DetailContainer>
+      <TransactionNodeDetailHeader
+        node={node}
+        organization={organization}
+        project={project}
+        event={event}
+        onTabScrollToNode={onTabScrollToNode}
+      />
+
+      <IssueList node={node} organization={organization} issues={issues} />
+
+      <Table
+        node={node}
+        onParentClick={onParentClick}
+        organization={organization}
+        event={event}
+        location={location}
+      />
+
+      <EventTags
+        node={node}
+        organization={organization}
+        event={event}
+        location={location}
+      />
+
+      <EventContexts event={event} />
+
+      {project ? <EventEvidence event={event} project={project} /> : null}
+
+      <ReplayPreview event={event} organization={organization} />
+
+      {event.projectSlug ? (
+        <Entries
+          definedEvent={event}
+          projectSlug={event.projectSlug}
+          group={undefined}
+          organization={organization}
+        />
+      ) : null}
+
+      <EventExtraData event={event} />
+
+      <EventSdk sdk={event.sdk} meta={event._meta?.sdk} />
+
+      {event._metrics_summary ? (
+        <CustomMetricsEventData
+          metricsSummary={event._metrics_summary}
+          startTimestamp={event.startTimestamp}
+        />
+      ) : null}
+
+      <BreadCrumbs event={event} organization={organization} />
+
+      {event.projectSlug ? (
+        <EventAttachments event={event} projectSlug={event.projectSlug} />
+      ) : null}
+
+      {project ? <EventViewHierarchy event={event} project={project} /> : null}
+
+      {event.projectSlug ? (
+        <EventRRWebIntegration
+          event={event}
+          orgId={organization.slug}
+          projectSlug={event.projectSlug}
+        />
+      ) : null}
+    </TraceDrawerComponents.DetailContainer>
+  );
+}

+ 67 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs.tsx

@@ -0,0 +1,67 @@
+import {Fragment, useEffect, useRef, useState} from 'react';
+
+import {Breadcrumbs} from 'sentry/components/events/interfaces/breadcrumbs';
+import {IconChevron} from 'sentry/icons/iconChevron';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {
+  type EntryBreadcrumbs,
+  EntryType,
+  type EventTransaction,
+  type Organization,
+} from 'sentry/types';
+
+export function BreadCrumbs({
+  event,
+  organization,
+}: {
+  event: EventTransaction;
+  organization: Organization;
+}) {
+  const [showBreadCrumbs, setShowBreadCrumbs] = useState(false);
+  const breadCrumbsContainerRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    setTimeout(() => {
+      if (showBreadCrumbs) {
+        breadCrumbsContainerRef.current?.scrollIntoView({
+          behavior: 'smooth',
+          block: 'nearest',
+        });
+      }
+    }, 100);
+  }, [showBreadCrumbs, breadCrumbsContainerRef]);
+
+  const matchingEntry: EntryBreadcrumbs | undefined = event?.entries?.find(
+    (entry): entry is EntryBreadcrumbs => entry.type === EntryType.BREADCRUMBS
+  );
+
+  if (!matchingEntry) {
+    return null;
+  }
+
+  const renderText = showBreadCrumbs ? t('Hide Breadcrumbs') : t('Show Breadcrumbs');
+  const chevron = <IconChevron size="xs" direction={showBreadCrumbs ? 'up' : 'down'} />;
+  return (
+    <Fragment>
+      <a
+        style={{display: 'flex', alignItems: 'center', gap: space(0.5)}}
+        onClick={() => {
+          setShowBreadCrumbs(prev => !prev);
+        }}
+      >
+        {renderText} {chevron}
+      </a>
+      <div ref={breadCrumbsContainerRef}>
+        {showBreadCrumbs && (
+          <Breadcrumbs
+            hideTitle
+            data={matchingEntry.data}
+            event={event}
+            organization={organization}
+          />
+        )}
+      </div>
+    </Fragment>
+  );
+}

+ 48 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/entries.tsx

@@ -0,0 +1,48 @@
+import {Fragment} from 'react';
+
+import {partitionEntriesForReplay} from 'sentry/components/events/eventEntries';
+import {EventEntry} from 'sentry/components/events/eventEntry';
+import {
+  EntryType,
+  type EventTransaction,
+  type Group,
+  type Organization,
+} from 'sentry/types';
+
+export function Entries({
+  definedEvent,
+  projectSlug,
+  group,
+  organization,
+}: {
+  definedEvent: EventTransaction;
+  group: Group | undefined;
+  organization: Organization;
+  projectSlug: string;
+}) {
+  if (!Array.isArray(definedEvent.entries) || !definedEvent.projectSlug) {
+    return null;
+  }
+
+  const [_, afterReplayEntries] = partitionEntriesForReplay(definedEvent.entries);
+
+  const eventEntryProps = {
+    projectSlug,
+    group,
+    organization,
+    event: definedEvent,
+    isShare: true,
+  };
+
+  return (
+    <Fragment>
+      {afterReplayEntries.map((entry, entryIdx) => {
+        if (entry.type === EntryType.BREADCRUMBS) {
+          return null;
+        }
+
+        return <EventEntry key={entryIdx} entry={entry} {...eventEntryProps} />;
+      })}
+    </Fragment>
+  );
+}

+ 56 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/opsBreakDown.tsx

@@ -0,0 +1,56 @@
+import {useState} from 'react';
+
+import {generateStats} from 'sentry/components/events/opsBreakdown';
+import PerformanceDuration from 'sentry/components/performanceDuration';
+import QuestionTooltip from 'sentry/components/questionTooltip';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {EventTransaction} from 'sentry/types';
+
+import {TraceDrawerComponents} from '../../styles';
+
+export function OpsBreakdown({event}: {event: EventTransaction}) {
+  const [showingAll, setShowingAll] = useState(false);
+  const breakdown = event && generateStats(event, {type: 'no_filter'});
+
+  if (breakdown.length <= 0) {
+    return null;
+  }
+
+  const renderText = showingAll ? t('Show less') : t('Show more') + '...';
+
+  return (
+    breakdown && (
+      <TraceDrawerComponents.TableRow
+        title={
+          <TraceDrawerComponents.FlexBox style={{gap: '5px'}}>
+            {t('Ops Breakdown')}
+            <QuestionTooltip
+              title={t('Applicable to the children of this event only')}
+              size="xs"
+            />
+          </TraceDrawerComponents.FlexBox>
+        }
+      >
+        <div style={{display: 'flex', flexDirection: 'column', gap: space(0.25)}}>
+          {breakdown.slice(0, showingAll ? breakdown.length : 5).map(currOp => {
+            const {name, percentage, totalInterval} = currOp;
+
+            const operationName = typeof name === 'string' ? name : t('Other');
+            const pctLabel = isFinite(percentage) ? Math.round(percentage * 100) : '∞';
+
+            return (
+              <div key={operationName}>
+                {operationName}:{' '}
+                <PerformanceDuration seconds={totalInterval} abbreviation /> ({pctLabel}%)
+              </div>
+            );
+          })}
+          {breakdown.length > 5 && (
+            <a onClick={() => setShowingAll(prev => !prev)}>{renderText}</a>
+          )}
+        </div>
+      </TraceDrawerComponents.TableRow>
+    )
+  );
+}

+ 74 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/replayPreview.tsx

@@ -0,0 +1,74 @@
+import styled from '@emotion/styled';
+
+import {REPLAY_CLIP_OFFSETS} from 'sentry/components/events/eventReplay';
+import ReplayClipPreview from 'sentry/components/events/eventReplay/replayClipPreview';
+import {LazyRender} from 'sentry/components/lazyRender';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {EventTransaction, Organization} from 'sentry/types';
+import {getAnalyticsDataForEvent} from 'sentry/utils/events';
+import {getReplayIdFromEvent} from 'sentry/utils/replays/getReplayIdFromEvent';
+
+import {TraceDrawerComponents} from '../../styles';
+
+function ReplaySection({
+  event,
+  organization,
+}: {
+  event: EventTransaction;
+  organization: Organization;
+}) {
+  const replayId = getReplayIdFromEvent(event);
+  const startTimestampMS =
+    event && 'startTimestamp' in event ? event.startTimestamp * 1000 : undefined;
+  const timeOfEvent = event.dateCreated ?? startTimestampMS ?? event.dateReceived;
+  const eventTimestampMs = timeOfEvent ? Math.floor(new Date(timeOfEvent).getTime()) : 0;
+
+  return replayId ? (
+    <ReplaySectionContainer>
+      <ReplaySectionTitle>{t('Session Replay')}</ReplaySectionTitle>
+      <ReplayClipPreview
+        analyticsContext="trace-view"
+        replaySlug={replayId}
+        orgSlug={organization.slug}
+        eventTimestampMs={eventTimestampMs}
+        clipOffsets={REPLAY_CLIP_OFFSETS}
+        fullReplayButtonProps={{
+          analyticsEventKey: 'trace-view.drawer-open-replay-details-clicked',
+          analyticsEventName: 'Trace View: Open Replay Details Clicked',
+          analyticsParams: {
+            ...getAnalyticsDataForEvent(event),
+            organization,
+          },
+        }}
+      />
+    </ReplaySectionContainer>
+  ) : null;
+}
+
+function ReplayPreview({
+  event,
+  organization,
+}: {
+  event: EventTransaction;
+  organization: Organization;
+}) {
+  return (
+    <LazyRender {...TraceDrawerComponents.LAZY_RENDER_PROPS} containerHeight={480}>
+      <ReplaySection event={event} organization={organization} />
+    </LazyRender>
+  );
+}
+
+const ReplaySectionContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+`;
+
+const ReplaySectionTitle = styled('div')`
+  font-size: ${p => p.theme.fontSizeMedium};
+  font-weight: 600;
+  margin-bottom: ${space(2)};
+`;
+
+export default ReplayPreview;

+ 282 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/table.tsx

@@ -0,0 +1,282 @@
+import {Fragment, useMemo} from 'react';
+import styled from '@emotion/styled';
+import type {Location} from 'history';
+import omit from 'lodash/omit';
+
+import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
+import {DateTime} from 'sentry/components/dateTime';
+import {
+  isNotMarkMeasurement,
+  isNotPerformanceScoreMeasurement,
+  TraceEventCustomPerformanceMetric,
+} from 'sentry/components/events/eventCustomPerformanceMetrics';
+import {getFormattedTimeRangeWithLeadingAndTrailingZero} from 'sentry/components/events/interfaces/spans/utils';
+import Link from 'sentry/components/links/link';
+import PerformanceDuration from 'sentry/components/performanceDuration';
+import {PAGE_URL_PARAM} from 'sentry/constants/pageFilters';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {EventTransaction, Organization} from 'sentry/types';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import getDynamicText from 'sentry/utils/getDynamicText';
+import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
+import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
+import {isCustomMeasurement} from 'sentry/views/dashboards/utils';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import {getTraceTabTitle} from 'sentry/views/performance/newTraceDetails/traceState/traceTabs';
+import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+
+import {useTraceAverageTransactionDuration} from '../../../../traceApi/useTraceAverageTransactionDuration';
+import {TraceDrawerComponents} from '../../styles';
+
+import {OpsBreakdown} from './opsBreakDown';
+
+function WebVitals({event}: {event: EventTransaction}) {
+  const measurementKeys = Object.keys(event?.measurements ?? {})
+    .filter(name => Boolean(WEB_VITAL_DETAILS[`measurements.${name}`]))
+    .sort();
+
+  if (!event || !event.measurements || measurementKeys.length <= 0) {
+    return null;
+  }
+
+  return (
+    <Fragment>
+      {measurementKeys.map(measurement => (
+        <TraceDrawerComponents.TableRow
+          key={measurement}
+          title={WEB_VITAL_DETAILS[`measurements.${measurement}`]?.name}
+        >
+          <PerformanceDuration
+            milliseconds={Number(event.measurements?.[measurement].value.toFixed(3))}
+            abbreviation
+          />
+        </TraceDrawerComponents.TableRow>
+      ))}
+    </Fragment>
+  );
+}
+
+function CustomPerformanceMetrics({event, location, organization}) {
+  const measurementNames = Object.keys(event.measurements ?? {})
+    .filter(name => isCustomMeasurement(`measurements.${name}`))
+    .filter(isNotMarkMeasurement)
+    .filter(isNotPerformanceScoreMeasurement)
+    .sort();
+
+  if (measurementNames.length <= 0) {
+    return null;
+  }
+
+  return (
+    <tr>
+      <td className="key">{t('Measurements')}</td>
+      <td className="value">
+        <Measurements>
+          {measurementNames.map(name => {
+            return (
+              event && (
+                <TraceEventCustomPerformanceMetric
+                  key={name}
+                  event={event}
+                  name={name}
+                  location={location}
+                  organization={organization}
+                  source={undefined}
+                  isHomepage={false}
+                />
+              )
+            );
+          })}
+        </Measurements>
+      </td>
+    </tr>
+  );
+}
+
+function DurationSummary({
+  node,
+  organization,
+  location,
+}: {
+  location: Location;
+  node: TraceTreeNode<TraceTree.Transaction>;
+  organization: Organization;
+}) {
+  const {data: averageDurationQueryResult} = useTraceAverageTransactionDuration({
+    node,
+    location,
+    organization,
+  });
+
+  const avgDurationInSeconds: number = useMemo(() => {
+    return (
+      Number(averageDurationQueryResult?.data?.[0]?.['avg(transaction.duration)']) / 1000
+    );
+  }, [averageDurationQueryResult]);
+
+  const startTimestamp = Math.min(node.value.start_timestamp, node.value.timestamp);
+  const endTimestamp = Math.max(node.value.start_timestamp, node.value.timestamp);
+  const durationInSeconds = endTimestamp - startTimestamp;
+
+  const {start: startTimeWithLeadingZero, end: endTimeWithLeadingZero} =
+    getFormattedTimeRangeWithLeadingAndTrailingZero(startTimestamp, endTimestamp);
+
+  return (
+    <Fragment>
+      <TraceDrawerComponents.TableRow title="Duration">
+        <TraceDrawerComponents.Duration
+          duration={durationInSeconds}
+          baseline={avgDurationInSeconds}
+          baseDescription={'Average duration for this transaction over the last 24 hours'}
+        />
+      </TraceDrawerComponents.TableRow>
+      <TraceDrawerComponents.TableRow title="Date Range">
+        {getDynamicText({
+          fixed: 'Mar 19, 2021 11:06:27 AM UTC',
+          value: (
+            <Fragment>
+              <DateTime date={startTimestamp * node.multiplier} />
+              {` (${startTimeWithLeadingZero})`}
+            </Fragment>
+          ),
+        })}
+        <br />
+        {getDynamicText({
+          fixed: 'Mar 19, 2021 11:06:28 AM UTC',
+          value: (
+            <Fragment>
+              <DateTime date={endTimestamp * node.multiplier} />
+              {` (${endTimeWithLeadingZero})`}
+            </Fragment>
+          ),
+        })}
+      </TraceDrawerComponents.TableRow>
+    </Fragment>
+  );
+}
+
+function EventSummary({
+  node,
+  onParentClick,
+  organization,
+  location,
+}: {
+  location: Location;
+  node: TraceTreeNode<TraceTree.Transaction>;
+  onParentClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
+  organization: Organization;
+}) {
+  const parentTransaction = node.parent_transaction;
+
+  return (
+    <Fragment>
+      {parentTransaction ? (
+        <TraceDrawerComponents.TableRow title="Parent Transaction">
+          <td className="value">
+            <a onClick={() => onParentClick(parentTransaction)}>
+              {getTraceTabTitle(parentTransaction)}
+            </a>
+          </td>
+        </TraceDrawerComponents.TableRow>
+      ) : null}
+      <TraceDrawerComponents.TableRow title={t('Event ID')}>
+        {node.value.event_id}
+        <CopyToClipboardButton
+          borderless
+          size="zero"
+          iconSize="xs"
+          text={node.value.event_id}
+        />
+      </TraceDrawerComponents.TableRow>
+      <TraceDrawerComponents.TableRow title={t('Description')}>
+        <Link
+          to={transactionSummaryRouteWithQuery({
+            orgSlug: organization.slug,
+            transaction: node.value.transaction,
+            query: omit(location.query, Object.values(PAGE_URL_PARAM)),
+            projectID: String(node.value.project_id),
+          })}
+        >
+          {node.value.transaction}
+        </Link>
+      </TraceDrawerComponents.TableRow>
+    </Fragment>
+  );
+}
+
+function ProfileLink({
+  node,
+  organization,
+}: {
+  node: TraceTreeNode<TraceTree.Transaction>;
+  organization: Organization;
+}) {
+  return node.value.profile_id ? (
+    <TraceDrawerComponents.TableRow
+      title="Profile ID"
+      extra={
+        <TraceDrawerComponents.Button
+          size="xs"
+          to={generateProfileFlamechartRoute({
+            orgSlug: organization.slug,
+            projectSlug: node.value.project_slug,
+            profileId: node.value.profile_id,
+          })}
+          onClick={function handleOnClick() {
+            trackAnalytics('profiling_views.go_to_flamegraph', {
+              organization,
+              source: 'performance.trace_view',
+            });
+          }}
+        >
+          {t('View Profile')}
+        </TraceDrawerComponents.Button>
+      }
+    >
+      {node.value.profile_id}
+    </TraceDrawerComponents.TableRow>
+  ) : null;
+}
+
+type TableProps = {
+  event: EventTransaction;
+  location: Location;
+  node: TraceTreeNode<TraceTree.Transaction>;
+  onParentClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
+  organization: Organization;
+};
+
+export function Table({node, onParentClick, organization, event, location}: TableProps) {
+  return (
+    <TraceDrawerComponents.Table className="table key-value">
+      <tbody>
+        <ProfileLink node={node} organization={organization} />
+        <DurationSummary node={node} organization={organization} location={location} />
+        <EventSummary
+          node={node}
+          onParentClick={onParentClick}
+          organization={organization}
+          location={location}
+        />
+        <OpsBreakdown event={event} />
+        <WebVitals event={event} />
+        <CustomPerformanceMetrics
+          event={event}
+          location={location}
+          organization={organization}
+        />
+      </tbody>
+    </TraceDrawerComponents.Table>
+  );
+}
+
+const Measurements = styled('div')`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${space(1)};
+  padding-top: 10px;
+`;

+ 53 - 0
static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/tags.tsx

@@ -0,0 +1,53 @@
+import styled from '@emotion/styled';
+import type {Location} from 'history';
+
+import NewTagsUI from 'sentry/components/events/eventTagsAndScreenshot/tags';
+import {LazyRender} from 'sentry/components/lazyRender';
+import type {EventTransaction, Organization} from 'sentry/types';
+import type {
+  TraceTree,
+  TraceTreeNode,
+} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
+import {Tags} from 'sentry/views/performance/traceDetails/styles';
+
+import {TraceDrawerComponents} from '../../styles';
+
+export function EventTags({
+  node,
+  organization,
+  event,
+  location,
+}: {
+  event: EventTransaction;
+  location: Location;
+  node: TraceTreeNode<TraceTree.Transaction>;
+  organization: Organization;
+}) {
+  return (
+    <LazyRender {...TraceDrawerComponents.LAZY_RENDER_PROPS} containerHeight={200}>
+      {organization.features.includes('event-tags-tree-ui') ? (
+        <TagsWrapper>
+          <NewTagsUI event={event} projectSlug={node.value.project_slug} />
+        </TagsWrapper>
+      ) : (
+        <TraceDrawerComponents.Table className="table key-value">
+          <tbody>
+            <Tags
+              enableHiding
+              location={location}
+              organization={organization}
+              tags={event.tags}
+              event={node.value}
+            />
+          </tbody>
+        </TraceDrawerComponents.Table>
+      )}
+    </LazyRender>
+  );
+}
+
+const TagsWrapper = styled('div')`
+  h3 {
+    color: ${p => p.theme.textColor};
+  }
+`;

+ 1 - 1
static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails.tsx

@@ -17,7 +17,7 @@ import {NoDataDetails} from '../details/noData';
 import {ParentAutogroupNodeDetails} from '../details/parentAutogroup';
 import {SiblingAutogroupNodeDetails} from '../details/siblingAutogroup';
 import {SpanNodeDetails} from '../details/span';
-import {TransactionNodeDetails} from '../details/transaction';
+import {TransactionNodeDetails} from '../details/transaction/index';
 
 export interface TraceTreeNodeDetailsProps<T> {
   manager: VirtualizedViewManager;