content.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. spanSlug,
  68. view,
  69. }),
  70. hideDefaultTabs: true,
  71. };
  72. return (
  73. <Fragment>
  74. {!isInDomainView && (
  75. <Layout.Header>
  76. <Layout.HeaderContent>
  77. <Breadcrumb
  78. organization={organization}
  79. location={location}
  80. transaction={{
  81. project: project?.id ?? '',
  82. name: transactionName,
  83. }}
  84. tab={Tab.SPANS}
  85. spanSlug={spanSlug}
  86. />
  87. <Layout.Title>
  88. {project && (
  89. <IdBadge
  90. project={project}
  91. avatarSize={28}
  92. hideName
  93. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  94. />
  95. )}
  96. {transactionName}
  97. </Layout.Title>
  98. </Layout.HeaderContent>
  99. </Layout.Header>
  100. )}
  101. {isInDomainView && view === 'frontend' && (
  102. <FrontendHeader {...domainViewHeaderProps} />
  103. )}
  104. {isInDomainView && view === 'backend' && (
  105. <BackendHeader {...domainViewHeaderProps} />
  106. )}
  107. {isInDomainView && view === 'mobile' && <MobileHeader {...domainViewHeaderProps} />}
  108. {isInDomainView && view === 'ai' && <AiHeader {...domainViewHeaderProps} />}
  109. <Layout.Body>
  110. <Layout.Main fullWidth>
  111. <SpanSummaryContent
  112. location={location}
  113. organization={organization}
  114. project={project}
  115. transactionName={transactionName}
  116. />
  117. </Layout.Main>
  118. </Layout.Body>
  119. </Fragment>
  120. );
  121. }
  122. type ContentProps = {
  123. location: Location;
  124. organization: Organization;
  125. project: Project | undefined;
  126. transactionName: string;
  127. };
  128. function SpanSummaryContent(props: ContentProps) {
  129. const {transactionName, project} = props;
  130. const {spanSlug: spanParam} = useParams();
  131. const [spanOp, groupId] = spanParam!.split(':');
  132. const filters: SpanMetricsQueryFilters = {
  133. 'span.group': groupId,
  134. 'span.op': spanOp,
  135. transaction: transactionName,
  136. };
  137. const {data: spanHeaderData} = useSpanMetrics(
  138. {
  139. search: MutableSearch.fromQueryObject(filters),
  140. fields: ['span.description', 'sum(span.duration)', 'count()'],
  141. sorts: [{field: 'sum(span.duration)', kind: 'desc'}],
  142. },
  143. SpanSummaryReferrer.SPAN_SUMMARY_HEADER_DATA
  144. );
  145. // Average span duration must be queried for separately, since it could get broken up into multiple groups if used in the first query
  146. const {data: avgDurationData} = useSpanMetrics(
  147. {
  148. search: MutableSearch.fromQueryObject(filters),
  149. fields: ['avg(span.duration)'],
  150. },
  151. SpanSummaryReferrer.SPAN_SUMMARY_HEADER_DATA
  152. );
  153. const parsedData = parseSpanHeaderData(spanHeaderData);
  154. return (
  155. <Fragment>
  156. <SpanSummaryControls />
  157. <SpanSummaryHeader
  158. spanOp={spanOp}
  159. spanDescription={parsedData?.description}
  160. avgDuration={avgDurationData[0]?.['avg(span.duration)']}
  161. timeSpent={parsedData?.timeSpent}
  162. spanCount={parsedData?.spanCount}
  163. />
  164. <SpanSummaryCharts />
  165. <SpanSummaryTable project={project} />
  166. </Fragment>
  167. );
  168. }
  169. function parseSpanHeaderData(data: Array<Partial<SpanMetricsResponse>>) {
  170. if (!data || data.length === 0) {
  171. return undefined;
  172. }
  173. if (data.length === 1) {
  174. return {
  175. description: data[0]?.['span.description'],
  176. timeSpent: data[0]?.['sum(span.duration)'],
  177. spanCount: data[0]?.['count()'],
  178. };
  179. }
  180. const cumulativeData = {
  181. description: undefined,
  182. timeSpent: 0,
  183. spanCount: 0,
  184. };
  185. data.forEach(datum => {
  186. cumulativeData.timeSpent += datum['sum(span.self_time)'] ?? 0;
  187. cumulativeData.spanCount += datum['count()'] ?? 0;
  188. });
  189. return cumulativeData;
  190. }