traceProfiles.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import Link from 'sentry/components/links/link';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import type {PlatformKey, Project} from 'sentry/types/project';
  8. import {trackAnalytics} from 'sentry/utils/analytics';
  9. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import useProjects from 'sentry/utils/useProjects';
  12. import {
  13. isSpanNode,
  14. isTransactionNode,
  15. } from 'sentry/views/performance/newTraceDetails/guards';
  16. import type {
  17. TraceTree,
  18. TraceTreeNode,
  19. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  20. export function TraceProfiles({
  21. tree,
  22. onScrollToNode,
  23. }: {
  24. onScrollToNode: (node: TraceTreeNode<any>) => void;
  25. tree: TraceTree;
  26. }) {
  27. const {projects} = useProjects();
  28. const organization = useOrganization();
  29. const projectLookup: Record<string, PlatformKey | undefined> = useMemo(() => {
  30. return projects.reduce<Record<Project['slug'], Project['platform']>>(
  31. (acc, project) => {
  32. acc[project.slug] = project.platform;
  33. return acc;
  34. },
  35. {}
  36. );
  37. }, [projects]);
  38. const profiles = useMemo(
  39. () => Array.from(tree.profiled_events.values()),
  40. [tree.profiled_events]
  41. );
  42. return (
  43. <ProfilesTable>
  44. <ProfilesTableRow>
  45. <ProfilesTableTitle>{t('Profiled Transaction')}</ProfilesTableTitle>
  46. <ProfilesTableTitle>{t('Profile')}</ProfilesTableTitle>
  47. </ProfilesTableRow>
  48. {profiles.map((node, index) => {
  49. const link = generateProfileFlamechartRouteWithQuery({
  50. orgSlug: organization.slug,
  51. projectSlug: node.metadata.project_slug as string,
  52. profileId: node.profiles?.[0].profile_id as string,
  53. });
  54. const onClick = () => {
  55. trackAnalytics('profiling_views.go_to_flamegraph', {
  56. organization,
  57. source: 'performance.trace_view',
  58. });
  59. };
  60. if (isTransactionNode(node)) {
  61. return (
  62. <ProfilesTableRow key={index}>
  63. <div>
  64. <a onClick={() => onScrollToNode(node)}>
  65. <PlatformIcon
  66. platform={projectLookup[node.value.project_slug] ?? 'default'}
  67. />
  68. <span>{node.value['transaction.op']}</span> —{' '}
  69. <span>{node.value.transaction}</span>
  70. </a>
  71. </div>
  72. <div>
  73. <Link to={link} onClick={onClick}>
  74. {node.profiles?.[0].profile_id?.substring(0, 8)}
  75. </Link>
  76. </div>
  77. </ProfilesTableRow>
  78. );
  79. }
  80. if (isSpanNode(node)) {
  81. return (
  82. <ProfilesTableRow key={index}>
  83. <div>
  84. <a onClick={() => onScrollToNode(node)}>
  85. <span>{node.value.op ?? '<unknown>'}</span> —{' '}
  86. <span className="TraceDescription" title={node.value.description}>
  87. {!node.value.description
  88. ? node.value.span_id ?? 'unknown'
  89. : node.value.description.length > 100
  90. ? node.value.description.slice(0, 100).trim() + '\u2026'
  91. : node.value.description}
  92. </span>
  93. </a>
  94. </div>
  95. <div>
  96. <Link to={link} onClick={onClick}>
  97. {node.profiles?.[0].profile_id?.substring(0, 8)}
  98. </Link>
  99. </div>
  100. </ProfilesTableRow>
  101. );
  102. }
  103. return null;
  104. })}
  105. </ProfilesTable>
  106. );
  107. }
  108. const ProfilesTable = styled('div')`
  109. margin-top: ${space(1)};
  110. display: grid !important;
  111. grid-template-columns: 1fr min-content;
  112. grid-template-rows: auto;
  113. width: 100%;
  114. border: 1px solid ${p => p.theme.border};
  115. border-radius: ${p => p.theme.borderRadius};
  116. overflow: hidden;
  117. > div {
  118. white-space: nowrap;
  119. overflow: hidden;
  120. text-overflow: ellipsis;
  121. padding: ${space(0.5)} ${space(1)};
  122. }
  123. img {
  124. width: 16px;
  125. height: 16px;
  126. margin-right: ${space(0.5)};
  127. }
  128. `;
  129. const ProfilesTableRow = styled('div')`
  130. display: grid;
  131. grid-column: 1 / -1;
  132. grid-template-columns: subgrid;
  133. width: 100%;
  134. padding: ${space(0.5)};
  135. padding: ${space(0.5)} ${space(2)};
  136. & > div {
  137. padding: ${space(0.5)} ${space(1)};
  138. white-space: nowrap;
  139. text-overflow: ellipsis;
  140. overflow: hidden;
  141. }
  142. &:first-child {
  143. background-color: ${p => p.theme.backgroundSecondary};
  144. }
  145. &:not(:last-child) {
  146. border-bottom: 1px solid ${p => p.theme.border};
  147. }
  148. `;
  149. const ProfilesTableTitle = styled('div')`
  150. color: ${p => p.theme.subText};
  151. font-size: ${p => p.theme.fontSizeMedium};
  152. font-weight: ${p => p.theme.fontWeightBold};
  153. padding: 0 ${space(0.5)};
  154. background-color: ${p => p.theme.backgroundSecondary};
  155. `;