content.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {Fragment} from 'react';
  2. import type {Location} from 'history';
  3. import IdBadge from 'sentry/components/idBadge';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import type {Organization} from 'sentry/types/organization';
  6. import type {Project} from 'sentry/types/project';
  7. import type {SpanSlug} from 'sentry/utils/performance/suspectSpans/types';
  8. import useRouteAnalyticsEventNames from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames';
  9. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  10. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import {useParams} from 'sentry/utils/useParams';
  13. import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
  14. import {AiHeader} from 'sentry/views/insights/pages/ai/aiPageHeader';
  15. import {BackendHeader} from 'sentry/views/insights/pages/backend/backendPageHeader';
  16. import {FrontendHeader} from 'sentry/views/insights/pages/frontend/frontendPageHeader';
  17. import {MobileHeader} from 'sentry/views/insights/pages/mobile/mobilePageHeader';
  18. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  19. import type {
  20. SpanMetricsQueryFilters,
  21. SpanMetricsResponse,
  22. } from 'sentry/views/insights/types';
  23. import Breadcrumb, {getTabCrumbs} from 'sentry/views/performance/breadcrumb';
  24. import {SpanSummaryReferrer} from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/referrers';
  25. import SpanSummaryCharts from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryCharts';
  26. import SpanSummaryTable from 'sentry/views/performance/transactionSummary/transactionSpans/spanSummary/spanSummaryTable';
  27. import {getSelectedProjectPlatforms} from 'sentry/views/performance/utils';
  28. import Tab from '../../tabs';
  29. import SpanSummaryControls from './spanSummaryControls';
  30. import SpanSummaryHeader from './spanSummaryHeader';
  31. type Props = {
  32. organization: Organization;
  33. project: Project | undefined;
  34. spanSlug: SpanSlug;
  35. transactionName: string;
  36. };
  37. export default function SpanSummary(props: Props) {
  38. const {organization, project, transactionName, spanSlug} = props;
  39. const location = useLocation();
  40. const {isInDomainView, view} = useDomainViewFilters();
  41. // customize the route analytics event we send
  42. useRouteAnalyticsEventNames(
  43. 'performance_views.span_summary.view',
  44. 'Performance Views: Span Summary page viewed'
  45. );
  46. useRouteAnalyticsParams({
  47. project_platforms: project ? getSelectedProjectPlatforms(location, [project]) : '',
  48. });
  49. const domainViewHeaderProps = {
  50. headerTitle: (
  51. <Fragment>
  52. {project && (
  53. <IdBadge
  54. project={project}
  55. avatarSize={28}
  56. hideName
  57. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  58. />
  59. )}
  60. {transactionName}
  61. </Fragment>
  62. ),
  63. breadcrumbs: getTabCrumbs({
  64. organization,
  65. location,
  66. transaction: {name: transactionName, project: project?.id ?? ''},
  67. tab: Tab.SPANS,
  68. spanSlug,
  69. view,
  70. }),
  71. hideDefaultTabs: true,
  72. };
  73. return (
  74. <Fragment>
  75. {!isInDomainView && (
  76. <Layout.Header>
  77. <Layout.HeaderContent>
  78. <Breadcrumb
  79. organization={organization}
  80. location={location}
  81. transaction={{
  82. project: project?.id ?? '',
  83. name: transactionName,
  84. }}
  85. tab={Tab.SPANS}
  86. spanSlug={spanSlug}
  87. />
  88. <Layout.Title>
  89. {project && (
  90. <IdBadge
  91. project={project}
  92. avatarSize={28}
  93. hideName
  94. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  95. />
  96. )}
  97. {transactionName}
  98. </Layout.Title>
  99. </Layout.HeaderContent>
  100. </Layout.Header>
  101. )}
  102. {isInDomainView && view === 'frontend' && (
  103. <FrontendHeader {...domainViewHeaderProps} />
  104. )}
  105. {isInDomainView && view === 'backend' && (
  106. <BackendHeader {...domainViewHeaderProps} />
  107. )}
  108. {isInDomainView && view === 'mobile' && <MobileHeader {...domainViewHeaderProps} />}
  109. {isInDomainView && view === 'ai' && <AiHeader {...domainViewHeaderProps} />}
  110. <Layout.Body>
  111. <Layout.Main fullWidth>
  112. <SpanSummaryContent
  113. location={location}
  114. organization={organization}
  115. project={project}
  116. transactionName={transactionName}
  117. />
  118. </Layout.Main>
  119. </Layout.Body>
  120. </Fragment>
  121. );
  122. }
  123. type ContentProps = {
  124. location: Location;
  125. organization: Organization;
  126. project: Project | undefined;
  127. transactionName: string;
  128. };
  129. function SpanSummaryContent(props: ContentProps) {
  130. const {transactionName, project} = props;
  131. const {spanSlug: spanParam} = useParams();
  132. const [spanOp, groupId] = spanParam.split(':');
  133. const filters: SpanMetricsQueryFilters = {
  134. 'span.group': groupId,
  135. 'span.op': spanOp,
  136. transaction: transactionName,
  137. };
  138. const {data: spanHeaderData} = useSpanMetrics(
  139. {
  140. search: MutableSearch.fromQueryObject(filters),
  141. fields: ['span.description', 'sum(span.duration)', 'count()'],
  142. sorts: [{field: 'sum(span.duration)', kind: 'desc'}],
  143. },
  144. SpanSummaryReferrer.SPAN_SUMMARY_HEADER_DATA
  145. );
  146. // Average span duration must be queried for separately, since it could get broken up into multiple groups if used in the first query
  147. const {data: avgDurationData} = useSpanMetrics(
  148. {
  149. search: MutableSearch.fromQueryObject(filters),
  150. fields: ['avg(span.duration)'],
  151. },
  152. SpanSummaryReferrer.SPAN_SUMMARY_HEADER_DATA
  153. );
  154. const parsedData = parseSpanHeaderData(spanHeaderData);
  155. return (
  156. <Fragment>
  157. <SpanSummaryControls />
  158. <SpanSummaryHeader
  159. spanOp={spanOp}
  160. spanDescription={parsedData?.description}
  161. avgDuration={avgDurationData[0]?.['avg(span.duration)']}
  162. timeSpent={parsedData?.timeSpent}
  163. spanCount={parsedData?.spanCount}
  164. />
  165. <SpanSummaryCharts />
  166. <SpanSummaryTable project={project} />
  167. </Fragment>
  168. );
  169. }
  170. function parseSpanHeaderData(data: Partial<SpanMetricsResponse>[]) {
  171. if (!data || data.length === 0) {
  172. return undefined;
  173. }
  174. if (data.length === 1) {
  175. return {
  176. description: data[0]?.['span.description'],
  177. timeSpent: data[0]?.['sum(span.duration)'],
  178. spanCount: data[0]?.['count()'],
  179. };
  180. }
  181. const cumulativeData = {
  182. description: undefined,
  183. timeSpent: 0,
  184. spanCount: 0,
  185. };
  186. data.forEach(datum => {
  187. cumulativeData.timeSpent += datum['sum(span.self_time)'] ?? 0;
  188. cumulativeData.spanCount += datum['count()'] ?? 0;
  189. });
  190. return cumulativeData;
  191. }