profilesTable.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import {Fragment} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import DateTime from 'sentry/components/dateTime';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnOrder,
  7. } from 'sentry/components/gridEditable';
  8. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  9. import Link from 'sentry/components/links/link';
  10. import PerformanceDuration from 'sentry/components/performanceDuration';
  11. import {IconCheckmark, IconClose} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {Trace} from 'sentry/types/profiling/core';
  14. import {defined} from 'sentry/utils';
  15. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  16. import {getShortEventId} from 'sentry/utils/events';
  17. import {
  18. generateProfileFlamechartRoute,
  19. generateProfileSummaryRouteWithQuery,
  20. } from 'sentry/utils/profiling/routes';
  21. import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import useOrganization from 'sentry/utils/useOrganization';
  24. import useProjects from 'sentry/utils/useProjects';
  25. const REQUIRE_PROJECT_COLUMNS: Set<TableColumnKey> = new Set([
  26. 'id',
  27. 'project_id',
  28. 'transaction_name',
  29. ]);
  30. interface ProfilesTableProps {
  31. error: string | null;
  32. isLoading: boolean;
  33. traces: Trace[];
  34. columnOrder?: Readonly<TableColumnKey[]>;
  35. }
  36. function ProfilesTable(props: ProfilesTableProps) {
  37. const location = useLocation();
  38. return (
  39. <Fragment>
  40. <GridEditable
  41. isLoading={props.isLoading}
  42. error={props.error}
  43. data={props.traces}
  44. columnOrder={(props.columnOrder ?? DEFAULT_COLUMN_ORDER).map(key => COLUMNS[key])}
  45. columnSortBy={[]}
  46. grid={{
  47. renderHeadCell: renderTableHead({rightAlignedColumns: RIGHT_ALIGNED_COLUMNS}),
  48. renderBodyCell: renderProfilesTableCell,
  49. }}
  50. location={location}
  51. />
  52. </Fragment>
  53. );
  54. }
  55. const RIGHT_ALIGNED_COLUMNS = new Set<TableColumnKey>(['trace_duration_ms']);
  56. function renderProfilesTableCell(
  57. column: TableColumn,
  58. dataRow: TableDataRow,
  59. rowIndex: number,
  60. columnIndex: number
  61. ) {
  62. return (
  63. <ProfilesTableCell
  64. column={column}
  65. dataRow={dataRow}
  66. rowIndex={rowIndex}
  67. columnIndex={columnIndex}
  68. />
  69. );
  70. }
  71. interface ProfilesTableCellProps {
  72. column: TableColumn;
  73. columnIndex: number;
  74. dataRow: TableDataRow;
  75. rowIndex: number;
  76. }
  77. function ProfilesTableCell({column, dataRow}: ProfilesTableCellProps) {
  78. const organization = useOrganization();
  79. const {projects} = useProjects();
  80. const location = useLocation();
  81. // Not all columns need the project, so small optimization to avoid
  82. // the linear lookup for every cell.
  83. const project = REQUIRE_PROJECT_COLUMNS.has(column.key)
  84. ? projects.find(proj => proj.id === dataRow.project_id)
  85. : undefined;
  86. if (REQUIRE_PROJECT_COLUMNS.has(column.key) && !defined(project)) {
  87. Sentry.withScope(scope => {
  88. scope.setFingerprint(['profiles table', 'cell', 'missing project']);
  89. scope.setTag('cell_key', column.key);
  90. scope.setTag('missing_project', dataRow.project_id);
  91. scope.setTag('available_project', projects.length);
  92. Sentry.captureMessage(`Project ${dataRow.project_id} missing for ${column.key}`);
  93. });
  94. }
  95. const value = dataRow[column.key];
  96. switch (column.key) {
  97. case 'id':
  98. if (!defined(project)) {
  99. // should never happen but just in case
  100. return <Container>{getShortEventId(dataRow.id)}</Container>;
  101. }
  102. const flamegraphTarget = generateProfileFlamechartRoute({
  103. orgSlug: organization.slug,
  104. projectSlug: project.slug,
  105. profileId: dataRow.id,
  106. });
  107. return (
  108. <Container>
  109. <Link to={flamegraphTarget}>{getShortEventId(dataRow.id)}</Link>
  110. </Container>
  111. );
  112. case 'project_id':
  113. if (!defined(project)) {
  114. // should never happen but just in case
  115. return <Container>{t('n/a')}</Container>;
  116. }
  117. return (
  118. <Container>
  119. <ProjectBadge project={project} avatarSize={16} />
  120. </Container>
  121. );
  122. case 'transaction_name':
  123. if (!defined(project)) {
  124. // should never happen but just in case
  125. return <Container>{t('n/a')}</Container>;
  126. }
  127. const profileSummaryTarget = generateProfileSummaryRouteWithQuery({
  128. query: location.query,
  129. orgSlug: organization.slug,
  130. projectSlug: project.slug,
  131. transaction: dataRow.transaction_name,
  132. });
  133. return (
  134. <Container>
  135. <Link to={profileSummaryTarget}>{value}</Link>
  136. </Container>
  137. );
  138. case 'version_name':
  139. return (
  140. <Container>
  141. {dataRow.version_code ? t('%s (build %s)', value, dataRow.version_code) : value}
  142. </Container>
  143. );
  144. case 'failed':
  145. return (
  146. <Container>
  147. {value ? (
  148. <IconClose size="sm" color="red300" isCircled />
  149. ) : (
  150. <IconCheckmark size="sm" color="green300" isCircled />
  151. )}
  152. </Container>
  153. );
  154. case 'timestamp':
  155. return (
  156. <Container>
  157. <DateTime date={value * 1000} year seconds timeZone />
  158. </Container>
  159. );
  160. case 'trace_duration_ms':
  161. return (
  162. <NumberContainer>
  163. <PerformanceDuration milliseconds={value} abbreviation />
  164. </NumberContainer>
  165. );
  166. default:
  167. return <Container>{value}</Container>;
  168. }
  169. }
  170. type TableColumnKey = keyof Trace;
  171. type NonTableColumnKey =
  172. | 'version_code'
  173. | 'device_locale'
  174. | 'device_manufacturer'
  175. | 'backtrace_available'
  176. | 'error_code'
  177. | 'error_code_name'
  178. | 'error_description'
  179. | 'span_annotations'
  180. | 'spans'
  181. | 'trace_annotations';
  182. type TableColumn = GridColumnOrder<TableColumnKey>;
  183. type TableDataRow = Omit<Record<TableColumnKey, any>, NonTableColumnKey> &
  184. Partial<Record<TableColumnKey, any>>;
  185. type TableColumnOrders = Omit<Record<TableColumnKey, TableColumn>, NonTableColumnKey>;
  186. const DEFAULT_COLUMN_ORDER: TableColumnKey[] = [
  187. 'failed',
  188. 'id',
  189. 'project_id',
  190. 'transaction_name',
  191. 'version_name',
  192. 'timestamp',
  193. 'trace_duration_ms',
  194. 'device_model',
  195. 'device_classification',
  196. ];
  197. const COLUMNS: TableColumnOrders = {
  198. id: {
  199. key: 'id',
  200. name: t('Profile ID'),
  201. width: COL_WIDTH_UNDEFINED,
  202. },
  203. project_id: {
  204. key: 'project_id',
  205. name: t('Project'),
  206. width: COL_WIDTH_UNDEFINED,
  207. },
  208. failed: {
  209. key: 'failed',
  210. name: t('Status'),
  211. width: 14, // make this as small as possible
  212. },
  213. version_name: {
  214. key: 'version_name',
  215. name: t('Version'),
  216. width: COL_WIDTH_UNDEFINED,
  217. },
  218. transaction_name: {
  219. key: 'transaction_name',
  220. name: t('Transaction Name'),
  221. width: COL_WIDTH_UNDEFINED,
  222. },
  223. timestamp: {
  224. key: 'timestamp',
  225. name: t('Timestamp'),
  226. width: COL_WIDTH_UNDEFINED,
  227. },
  228. trace_duration_ms: {
  229. key: 'trace_duration_ms',
  230. name: t('Duration'),
  231. width: COL_WIDTH_UNDEFINED,
  232. },
  233. device_model: {
  234. key: 'device_model',
  235. name: t('Device Model'),
  236. width: COL_WIDTH_UNDEFINED,
  237. },
  238. device_classification: {
  239. key: 'device_classification',
  240. name: t('Device Classification'),
  241. width: COL_WIDTH_UNDEFINED,
  242. },
  243. device_os_version: {
  244. key: 'device_os_version',
  245. name: t('Device OS Version'),
  246. width: COL_WIDTH_UNDEFINED,
  247. },
  248. };
  249. export {ProfilesTable};