profileEventsTable.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import {useCallback} from 'react';
  2. import {Location} from 'history';
  3. import Count from 'sentry/components/count';
  4. import DateTime from 'sentry/components/dateTime';
  5. import GridEditable, {
  6. COL_WIDTH_UNDEFINED,
  7. GridColumnOrder,
  8. GridColumnSortBy,
  9. } from 'sentry/components/gridEditable';
  10. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  11. import Link from 'sentry/components/links/link';
  12. import PerformanceDuration from 'sentry/components/performanceDuration';
  13. import UserMisery from 'sentry/components/userMisery';
  14. import Version from 'sentry/components/version';
  15. import {t} from 'sentry/locale';
  16. import {Organization, Project} from 'sentry/types';
  17. import {defined} from 'sentry/utils';
  18. import {DURATION_UNITS} from 'sentry/utils/discover/fieldRenderers';
  19. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  20. import {getShortEventId} from 'sentry/utils/events';
  21. import {EventsResults} from 'sentry/utils/profiling/hooks/types';
  22. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  23. import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import useOrganization from 'sentry/utils/useOrganization';
  26. import useProjects from 'sentry/utils/useProjects';
  27. import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
  28. import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
  29. import {ProfilingTransactionHovercard} from './profilingTransactionHovercard';
  30. interface ProfileEventsTableProps<F extends FieldType> {
  31. columns: readonly F[];
  32. data: EventsResults<F> | null;
  33. error: string | null;
  34. isLoading: boolean;
  35. sort: GridColumnSortBy<F>;
  36. sortableColumns?: Set<F>;
  37. }
  38. export function ProfileEventsTable<F extends FieldType>(
  39. props: ProfileEventsTableProps<F>
  40. ) {
  41. const location = useLocation();
  42. const organization = useOrganization();
  43. const {projects} = useProjects();
  44. const generateSortLink = useCallback(
  45. (column: F) => () => {
  46. let dir = 'desc';
  47. if (column === props.sort.key && props.sort.order === dir) {
  48. dir = 'asc';
  49. }
  50. return {
  51. ...location,
  52. query: {
  53. ...location.query,
  54. sort: dir === 'asc' ? column : `-${column}`,
  55. },
  56. };
  57. },
  58. [location, props.sort]
  59. );
  60. return (
  61. <GridEditable
  62. isLoading={props.isLoading}
  63. error={props.error}
  64. data={props.data?.data ?? []}
  65. columnOrder={props.columns.map(field => getColumnOrder<F>(field))}
  66. columnSortBy={[props.sort]}
  67. grid={{
  68. renderHeadCell: renderTableHead<F>({
  69. currentSort: props.sort,
  70. generateSortLink,
  71. rightAlignedColumns: getRightAlignedColumns(props.columns),
  72. sortableColumns: props.sortableColumns,
  73. }),
  74. renderBodyCell: renderTableBody(
  75. props.data?.meta ?? ({fields: {}, units: {}} as EventsResults<F>['meta']),
  76. {location, organization, projects}
  77. ),
  78. }}
  79. location={location}
  80. />
  81. );
  82. }
  83. type RenderBagger = {
  84. location: Location;
  85. organization: Organization;
  86. projects: Project[];
  87. };
  88. function renderTableBody<F extends FieldType>(
  89. meta: EventsResults<F>['meta'],
  90. baggage: RenderBagger
  91. ) {
  92. function _renderTableBody(
  93. column: GridColumnOrder<F>,
  94. dataRow: Record<F, any>,
  95. rowIndex: number,
  96. columnIndex: number
  97. ) {
  98. return (
  99. <ProfileEventsCell
  100. meta={meta}
  101. baggage={baggage}
  102. column={column}
  103. dataRow={dataRow}
  104. rowIndex={rowIndex}
  105. columnIndex={columnIndex}
  106. />
  107. );
  108. }
  109. return _renderTableBody;
  110. }
  111. interface ProfileEventsCellProps<F extends FieldType> {
  112. baggage: RenderBagger;
  113. column: GridColumnOrder<F>;
  114. columnIndex: number;
  115. dataRow: Record<F, any>;
  116. meta: EventsResults<F>['meta'];
  117. rowIndex: number;
  118. }
  119. function ProfileEventsCell<F extends FieldType>(props: ProfileEventsCellProps<F>) {
  120. const key = props.column.key;
  121. const value = props.dataRow[key];
  122. const columnType = props.meta.fields[key];
  123. const columnUnit = props.meta.units[key];
  124. if (key === 'id' || key === 'profile.id') {
  125. const project = getProjectForRow(props.baggage, props.dataRow);
  126. if (!defined(project)) {
  127. // should never happen but just in case
  128. return <Container>{getShortEventId(value)}</Container>;
  129. }
  130. const flamegraphTarget = generateProfileFlamechartRoute({
  131. orgSlug: props.baggage.organization.slug,
  132. projectSlug: project.slug,
  133. profileId: value,
  134. });
  135. return (
  136. <Container>
  137. <Link to={flamegraphTarget}>{getShortEventId(value)}</Link>
  138. </Container>
  139. );
  140. }
  141. if (key === 'trace') {
  142. const traceId = getShortEventId(props.dataRow[key] ?? '');
  143. if (!traceId) {
  144. return <Container>{t('n/a')}</Container>;
  145. }
  146. return (
  147. <Container>
  148. <Link to={`/performance/trace/${props.dataRow[key]}`}>{traceId}</Link>
  149. </Container>
  150. );
  151. }
  152. if (key === 'trace.transaction') {
  153. const project = getProjectForRow(props.baggage, props.dataRow);
  154. const transactionId = getShortEventId(props.dataRow[key] ?? '');
  155. if (!project) {
  156. return <Container>{transactionId}</Container>;
  157. }
  158. return (
  159. <Container>
  160. <Link to={`/performance/${project.slug}:${props.dataRow[key]}`}>
  161. {transactionId}
  162. </Link>
  163. </Container>
  164. );
  165. }
  166. if (key === 'project.id' || key === 'project' || key === 'project.name') {
  167. const project = getProjectForRow(props.baggage, props.dataRow);
  168. if (!defined(project)) {
  169. // should never happen but just in case
  170. return <Container>{t('n/a')}</Container>;
  171. }
  172. return (
  173. <Container>
  174. <ProjectBadge project={project} avatarSize={16} />
  175. </Container>
  176. );
  177. }
  178. if (key === 'transaction') {
  179. const project = getProjectForRow(props.baggage, props.dataRow);
  180. if (defined(project)) {
  181. return (
  182. <Container>
  183. <ProfilingTransactionHovercard
  184. transaction={value}
  185. project={project}
  186. organization={props.baggage.organization}
  187. />
  188. </Container>
  189. );
  190. }
  191. // let this fall through and use one of the other renderers
  192. }
  193. if (key === 'release') {
  194. if (value) {
  195. return (
  196. <QuickContextHoverWrapper
  197. dataRow={props.dataRow}
  198. contextType={ContextType.RELEASE}
  199. organization={props.baggage.organization}
  200. >
  201. <Version version={value} truncate />
  202. </QuickContextHoverWrapper>
  203. );
  204. }
  205. }
  206. if (key === 'user_misery()') {
  207. return (
  208. <UserMisery
  209. bars={10}
  210. barHeight={20}
  211. miserableUsers={undefined}
  212. miseryLimit={undefined}
  213. totalUsers={undefined}
  214. userMisery={value || 0}
  215. />
  216. );
  217. }
  218. switch (columnType) {
  219. case 'integer':
  220. case 'number':
  221. return (
  222. <NumberContainer>
  223. <Count value={value} />
  224. </NumberContainer>
  225. );
  226. case 'duration':
  227. const multiplier = columnUnit ? DURATION_UNITS[columnUnit as string] ?? 1 : 1;
  228. return (
  229. <NumberContainer>
  230. <PerformanceDuration milliseconds={value * multiplier} abbreviation />
  231. </NumberContainer>
  232. );
  233. case 'date':
  234. return (
  235. <Container>
  236. <DateTime date={value} year seconds timeZone />
  237. </Container>
  238. );
  239. default:
  240. return <Container>{value}</Container>;
  241. }
  242. }
  243. function getProjectForRow<F extends FieldType>(
  244. baggage: ProfileEventsCellProps<F>['baggage'],
  245. dataRow: ProfileEventsCellProps<F>['dataRow']
  246. ) {
  247. let project: Project | undefined = undefined;
  248. if (defined(dataRow['project.id'])) {
  249. const projectId = dataRow['project.id'].toString();
  250. project = baggage.projects.find(proj => proj.id === projectId);
  251. } else if (defined((dataRow as any).project)) {
  252. const projectSlug = (dataRow as any).project;
  253. project = baggage.projects.find(proj => proj.slug === projectSlug);
  254. } else if (defined(dataRow['project.name'])) {
  255. const projectSlug = dataRow['project.name'];
  256. project = baggage.projects.find(proj => proj.slug === projectSlug);
  257. }
  258. return project ?? null;
  259. }
  260. const FIELDS = [
  261. 'id',
  262. 'profile.id',
  263. 'trace.transaction',
  264. 'trace',
  265. 'transaction',
  266. 'transaction.duration',
  267. 'profile.duration',
  268. 'project',
  269. 'project.id',
  270. 'project.name',
  271. 'environment',
  272. 'timestamp',
  273. 'release',
  274. 'platform.name',
  275. 'device.arch',
  276. 'device.classification',
  277. 'device.locale',
  278. 'device.manufacturer',
  279. 'device.model',
  280. 'os.build',
  281. 'os.name',
  282. 'os.version',
  283. 'last_seen()',
  284. 'p75()',
  285. 'p95()',
  286. 'p99()',
  287. 'count()',
  288. 'user_misery()',
  289. ] as const;
  290. type FieldType = (typeof FIELDS)[number];
  291. const RIGHT_ALIGNED_FIELDS = new Set<FieldType>([
  292. 'transaction.duration',
  293. 'profile.duration',
  294. 'p75()',
  295. 'p95()',
  296. 'p99()',
  297. 'count()',
  298. ]);
  299. // TODO: add all the columns here
  300. const COLUMN_ORDERS: Record<FieldType, GridColumnOrder<FieldType>> = {
  301. id: {
  302. key: 'id',
  303. name: t('Profile ID'),
  304. width: COL_WIDTH_UNDEFINED,
  305. },
  306. 'profile.id': {
  307. key: 'profile.id',
  308. name: t('Profile ID'),
  309. width: COL_WIDTH_UNDEFINED,
  310. },
  311. transaction: {
  312. key: 'transaction',
  313. name: t('Transaction'),
  314. width: COL_WIDTH_UNDEFINED,
  315. },
  316. 'transaction.duration': {
  317. key: 'transaction.duration',
  318. name: t('Duration'),
  319. width: COL_WIDTH_UNDEFINED,
  320. },
  321. trace: {
  322. key: 'trace',
  323. name: t('Trace ID'),
  324. width: COL_WIDTH_UNDEFINED,
  325. },
  326. 'trace.transaction': {
  327. key: 'trace.transaction',
  328. name: t('Transaction ID'),
  329. width: COL_WIDTH_UNDEFINED,
  330. },
  331. 'profile.duration': {
  332. key: 'profile.duration',
  333. name: t('Duration'),
  334. width: COL_WIDTH_UNDEFINED,
  335. },
  336. project: {
  337. key: 'project',
  338. name: t('Project'),
  339. width: COL_WIDTH_UNDEFINED,
  340. },
  341. 'project.id': {
  342. key: 'project.id',
  343. name: t('Project'),
  344. width: COL_WIDTH_UNDEFINED,
  345. },
  346. 'project.name': {
  347. key: 'project.name',
  348. name: t('Project'),
  349. width: COL_WIDTH_UNDEFINED,
  350. },
  351. environment: {
  352. key: 'environment',
  353. name: t('Environment'),
  354. width: COL_WIDTH_UNDEFINED,
  355. },
  356. timestamp: {
  357. key: 'timestamp',
  358. name: t('Timestamp'),
  359. width: COL_WIDTH_UNDEFINED,
  360. },
  361. release: {
  362. key: 'release',
  363. name: t('Release'),
  364. width: COL_WIDTH_UNDEFINED,
  365. },
  366. 'platform.name': {
  367. key: 'platform.name',
  368. name: t('Platform'),
  369. width: COL_WIDTH_UNDEFINED,
  370. },
  371. 'device.arch': {
  372. key: 'device.arch',
  373. name: t('Device Architecture'),
  374. width: COL_WIDTH_UNDEFINED,
  375. },
  376. 'device.classification': {
  377. key: 'device.classification',
  378. name: t('Device Classification'),
  379. width: COL_WIDTH_UNDEFINED,
  380. },
  381. 'device.locale': {
  382. key: 'device.locale',
  383. name: t('Device Locale'),
  384. width: COL_WIDTH_UNDEFINED,
  385. },
  386. 'device.manufacturer': {
  387. key: 'device.manufacturer',
  388. name: t('Device Manufacturer'),
  389. width: COL_WIDTH_UNDEFINED,
  390. },
  391. 'device.model': {
  392. key: 'device.model',
  393. name: t('Device Model'),
  394. width: COL_WIDTH_UNDEFINED,
  395. },
  396. 'os.build': {
  397. key: 'os.build',
  398. name: t('OS Build'),
  399. width: COL_WIDTH_UNDEFINED,
  400. },
  401. 'os.name': {
  402. key: 'os.name',
  403. name: t('OS Name'),
  404. width: COL_WIDTH_UNDEFINED,
  405. },
  406. 'os.version': {
  407. key: 'os.version',
  408. name: t('OS Version'),
  409. width: COL_WIDTH_UNDEFINED,
  410. },
  411. 'last_seen()': {
  412. key: 'last_seen()',
  413. name: t('Last Seen'),
  414. width: COL_WIDTH_UNDEFINED,
  415. },
  416. 'p75()': {
  417. key: 'p75()',
  418. name: t('P75'),
  419. width: COL_WIDTH_UNDEFINED,
  420. },
  421. 'p95()': {
  422. key: 'p95()',
  423. name: t('P95()'),
  424. width: COL_WIDTH_UNDEFINED,
  425. },
  426. 'p99()': {
  427. key: 'p99()',
  428. name: t('P99()'),
  429. width: COL_WIDTH_UNDEFINED,
  430. },
  431. 'count()': {
  432. key: 'count()',
  433. name: t('Count()'),
  434. width: COL_WIDTH_UNDEFINED,
  435. },
  436. 'user_misery()': {
  437. key: 'user_misery()',
  438. name: t('User Misery'),
  439. width: 110,
  440. },
  441. };
  442. function getColumnOrder<F extends FieldType>(field: F): GridColumnOrder<F> {
  443. if (COLUMN_ORDERS[field as string]) {
  444. return COLUMN_ORDERS[field as string] as GridColumnOrder<F>;
  445. }
  446. return {
  447. key: field,
  448. name: field,
  449. width: COL_WIDTH_UNDEFINED,
  450. };
  451. }
  452. function getRightAlignedColumns<F extends FieldType>(columns: readonly F[]): Set<F> {
  453. return new Set(columns.filter(col => RIGHT_ALIGNED_FIELDS.has(col)));
  454. }