Browse Source

feat(trace-explorer): Improve rendering of cells in trace explorer (#67730)

This improves the renderering of some cells in the trace explorer to
format the values in a more readable way and link to appropriate
resources.
Tony Xiao 11 months ago
parent
commit
20de5ec75a

+ 3 - 2
static/app/views/performance/traces/content.tsx

@@ -60,14 +60,15 @@ export function Content() {
     return [
       SpanIndexedField.PROJECT,
       SpanIndexedField.ID,
+      SpanIndexedField.TRANSACTION_ID,
       SpanIndexedField.TRACE,
       SpanIndexedField.SPAN_OP,
       SpanIndexedField.SPAN_DESCRIPTION,
       SpanIndexedField.TRANSACTION_OP,
       SpanIndexedField.TRANSACTION,
-      SpanIndexedField.TIMESTAMP,
       SpanIndexedField.SPAN_DURATION,
       SpanIndexedField.SPAN_SELF_TIME,
+      SpanIndexedField.TIMESTAMP,
     ];
   }, []);
 
@@ -76,7 +77,7 @@ export function Content() {
     filters,
     limit,
     sorts: [],
-    referrer: '',
+    referrer: 'api.trace-explorer.table',
   });
 
   return (

+ 283 - 0
static/app/views/performance/traces/table/fieldRenderers.tsx

@@ -0,0 +1,283 @@
+import type {FC, ReactText} from 'react';
+
+import type {GridColumnOrder} from 'sentry/components/gridEditable';
+import ProjectBadge from 'sentry/components/idBadge/projectBadge';
+import Link from 'sentry/components/links/link';
+import PerformanceDuration from 'sentry/components/performanceDuration';
+import type {DateString} from 'sentry/types';
+import {defined} from 'sentry/utils';
+import {Container, FieldDateTime} from 'sentry/utils/discover/styles';
+import {getShortEventId} from 'sentry/utils/events';
+import {getTransactionDetailsUrl} from 'sentry/utils/performance/urls';
+import Projects from 'sentry/utils/projects';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjects from 'sentry/utils/useProjects';
+import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
+import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+
+import type {ColumnKey, DataRow} from './types';
+
+interface FieldRendererProps {
+  column: GridColumnOrder<ColumnKey>;
+  row: DataRow;
+}
+
+export function getFieldRenderer(field: ColumnKey): FC<FieldRendererProps> {
+  return fieldRenderers[field] ?? DefaultRenderer;
+}
+
+const fieldRenderers: Record<ReactText, FC<FieldRendererProps>> = {
+  project: ProjectRenderer,
+  span_id: SpanIdRenderer,
+  'span.duration': SpanDurationRenderer,
+  'span.self_time': SpanSelfTimeRenderer,
+  timestamp: TimestampRenderer,
+  trace: TraceIdRenderer,
+  transaction: TransactionRenderer,
+  'transaction.id': TransactionIdRenderer,
+};
+
+function DefaultRenderer({row, column}: FieldRendererProps) {
+  // TODO: this can be smarter based on the type of the value
+  return <Container>{row[column.key]}</Container>;
+}
+
+function ProjectRenderer(props: FieldRendererProps) {
+  const projectSlug = props.row.project;
+
+  if (!defined(projectSlug)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return <_ProjectRenderer {...props} projectSlug={projectSlug} />;
+}
+
+interface ProjectRendererProps extends FieldRendererProps {
+  projectSlug: string;
+}
+
+function _ProjectRenderer({projectSlug}: ProjectRendererProps) {
+  const organization = useOrganization();
+
+  return (
+    <Container>
+      <Projects orgId={organization.slug} slugs={[projectSlug]}>
+        {({projects}) => {
+          const project = projects.find(p => p.slug === projectSlug);
+          return (
+            <ProjectBadge
+              project={project ? project : {slug: projectSlug}}
+              avatarSize={16}
+            />
+          );
+        }}
+      </Projects>
+    </Container>
+  );
+}
+
+function SpanIdRenderer(props: FieldRendererProps) {
+  const projectSlug = props.row.project;
+  const spanId = props.row.span_id;
+  const transactionId = props.row['transaction.id'];
+
+  if (!defined(projectSlug) || !defined(spanId) || !defined(transactionId)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return (
+    <_SpanIdRenderer
+      {...props}
+      projectSlug={projectSlug}
+      spanId={spanId}
+      transactionId={transactionId}
+    />
+  );
+}
+
+interface _SpanIdRendererProps extends FieldRendererProps {
+  projectSlug: string;
+  spanId: string;
+  transactionId: string;
+}
+
+function _SpanIdRenderer({projectSlug, spanId, transactionId}: _SpanIdRendererProps) {
+  const organization = useOrganization();
+
+  const target = getTransactionDetailsUrl(
+    organization.slug,
+    `${projectSlug}:${transactionId}`,
+    undefined,
+    undefined,
+    spanId
+  );
+
+  return <Link to={target}>{getShortEventId(spanId)}</Link>;
+}
+
+function TraceIdRenderer(props: FieldRendererProps) {
+  const traceId = props.row.trace;
+
+  if (!defined(traceId)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return (
+    <_TraceIdRenderer
+      {...props}
+      traceId={traceId}
+      transactionId={props.row['transaction.id'] ?? undefined}
+      timestamp={props.row.timestamp}
+    />
+  );
+}
+
+interface TraceIdRendererProps extends FieldRendererProps {
+  traceId: string;
+  timestamp?: DateString;
+  transactionId?: string;
+}
+
+function _TraceIdRenderer({traceId, timestamp, transactionId}: TraceIdRendererProps) {
+  const organization = useOrganization();
+  const {selection} = usePageFilters();
+  const stringOrNumberTimestamp =
+    timestamp instanceof Date ? timestamp.toISOString() : timestamp ?? '';
+
+  const target = getTraceDetailsUrl(
+    organization,
+    traceId,
+    {
+      start: selection.datetime.start,
+      end: selection.datetime.end,
+      statsPeriod: selection.datetime.period,
+    },
+    {},
+    stringOrNumberTimestamp,
+    transactionId
+  );
+
+  return (
+    <Container>
+      <Link to={target}>{getShortEventId(traceId)}</Link>
+    </Container>
+  );
+}
+
+function TransactionIdRenderer(props: FieldRendererProps) {
+  const projectSlug = props.row.project;
+  const transactionId = props.row['transaction.id'];
+
+  if (!defined(projectSlug) || !defined(transactionId)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return (
+    <_TransactionIdRenderer
+      {...props}
+      projectSlug={projectSlug}
+      transactionId={transactionId}
+    />
+  );
+}
+
+interface TransactionIdRendererProps extends FieldRendererProps {
+  projectSlug: string;
+  transactionId: string;
+}
+
+function _TransactionIdRenderer({
+  projectSlug,
+  transactionId,
+}: TransactionIdRendererProps) {
+  const organization = useOrganization();
+
+  const target = getTransactionDetailsUrl(
+    organization.slug,
+    `${projectSlug}:${transactionId}`,
+    undefined,
+    undefined
+  );
+
+  return <Link to={target}>{getShortEventId(transactionId)}</Link>;
+}
+
+function TransactionRenderer(props: FieldRendererProps) {
+  const projectSlug = props.row.project;
+  const transaction = props.row.transaction;
+
+  if (!defined(projectSlug) || !defined(transaction)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return (
+    <_TransactionRenderer
+      {...props}
+      projectSlug={projectSlug}
+      transaction={transaction}
+    />
+  );
+}
+
+interface TransactionRendererProps {
+  projectSlug: string;
+  transaction: string;
+}
+
+function _TransactionRenderer({projectSlug, transaction}: TransactionRendererProps) {
+  const location = useLocation();
+  const organization = useOrganization();
+  const {projects} = useProjects({slugs: [projectSlug]});
+
+  const target = transactionSummaryRouteWithQuery({
+    orgSlug: organization.slug,
+    transaction,
+    query: {
+      ...location.query,
+      query: undefined,
+    },
+    projectID: String(projects[0]?.id ?? ''),
+  });
+
+  return (
+    <Container>
+      <Link to={target}>{transaction}</Link>
+    </Container>
+  );
+}
+
+function TimestampRenderer(props: FieldRendererProps) {
+  const location = useLocation();
+  const timestamp = props.row.timestamp;
+
+  if (!defined(timestamp)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  const utc = decodeScalar(location?.query?.utc) === 'true';
+
+  return <FieldDateTime date={timestamp} year seconds timeZone utc={utc} />;
+}
+
+function SpanDurationRenderer(props: FieldRendererProps) {
+  const duration = props.row['span.duration'];
+
+  if (!defined(duration)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return <PerformanceDuration milliseconds={duration} abbreviation />;
+}
+
+function SpanSelfTimeRenderer(props: FieldRendererProps) {
+  const duration = props.row['span.duration'];
+
+  if (!defined(duration)) {
+    return <DefaultRenderer {...props} />;
+  }
+
+  return <PerformanceDuration milliseconds={duration} abbreviation />;
+}

+ 9 - 0
static/app/views/performance/traces/table/types.tsx

@@ -0,0 +1,9 @@
+import type {ReactText} from 'react';
+
+import type {IndexedResponse} from 'sentry/views/starfish/types';
+
+export type ColumnKey = ReactText;
+
+type DataType = string[] | string | number | null;
+
+export type DataRow = Partial<IndexedResponse> & {[key: string]: DataType};

+ 9 - 5
static/app/views/performance/traces/tracesSpansTable.tsx

@@ -1,14 +1,17 @@
 import {Fragment, useMemo} from 'react';
 import styled from '@emotion/styled';
 
+import type {GridColumnOrder} from 'sentry/components/gridEditable';
 import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
 import type {CursorHandler} from 'sentry/components/pagination';
 import Pagination from 'sentry/components/pagination';
-import {Container} from 'sentry/utils/discover/styles';
 import {useLocation} from 'sentry/utils/useLocation';
 
+import {getFieldRenderer} from './table/fieldRenderers';
+import type {ColumnKey, DataRow} from './table/types';
+
 interface TracesSpansTableProps {
-  data: any[];
+  data: DataRow[];
   fields: string[];
   handleCursor: CursorHandler;
   isLoading: boolean;
@@ -24,7 +27,7 @@ export function TracesSpansTable({
 }: TracesSpansTableProps) {
   const location = useLocation();
 
-  const columnOrder = useMemo(() => {
+  const columnOrder: GridColumnOrder<ColumnKey>[] = useMemo(() => {
     return fields.map(field => {
       return {
         key: field,
@@ -52,8 +55,9 @@ export function TracesSpansTable({
 }
 
 function renderBodyCell() {
-  return function (col, row) {
-    return <Container>{row[col.key]}</Container>;
+  return function (column: GridColumnOrder<ColumnKey>, row: DataRow) {
+    const Renderer = getFieldRenderer(column.key);
+    return <Renderer column={column} row={row} />;
   };
 }
 

+ 3 - 0
static/app/views/starfish/types.tsx

@@ -143,6 +143,7 @@ export enum SpanIndexedField {
 }
 
 export type IndexedResponse = {
+  [SpanIndexedField.SPAN_DURATION]: number;
   [SpanIndexedField.SPAN_SELF_TIME]: number;
   [SpanIndexedField.SPAN_GROUP]: string;
   [SpanIndexedField.SPAN_MODULE]: string;
@@ -150,6 +151,8 @@ export type IndexedResponse = {
   [SpanIndexedField.SPAN_OP]: string;
   [SpanIndexedField.ID]: string;
   [SpanIndexedField.SPAN_ACTION]: string;
+  [SpanIndexedField.TRACE]: string;
+  [SpanIndexedField.TRANSACTION]: string;
   [SpanIndexedField.TRANSACTION_ID]: string;
   [SpanIndexedField.TRANSACTION_METHOD]: string;
   [SpanIndexedField.TRANSACTION_OP]: string;