index.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. import {useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {Button, LinkButton} from 'sentry/components/button';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import type {SelectOption} from 'sentry/components/compactSelect/types';
  7. import Count from 'sentry/components/count';
  8. import {DateTime} from 'sentry/components/dateTime';
  9. import type {SmartSearchBarProps} from 'sentry/components/deprecatedSmartSearchBar';
  10. import ErrorBoundary from 'sentry/components/errorBoundary';
  11. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  12. import IdBadge from 'sentry/components/idBadge';
  13. import * as Layout from 'sentry/components/layouts/thirds';
  14. import Link from 'sentry/components/links/link';
  15. import LoadingIndicator from 'sentry/components/loadingIndicator';
  16. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  17. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  18. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  19. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  20. import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  21. import PerformanceDuration from 'sentry/components/performanceDuration';
  22. import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
  23. import {AggregateFlamegraphTreeTable} from 'sentry/components/profiling/flamegraph/aggregateFlamegraphTreeTable';
  24. import {FlamegraphSearch} from 'sentry/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch';
  25. import type {ProfilingBreadcrumbsProps} from 'sentry/components/profiling/profilingBreadcrumbs';
  26. import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs';
  27. import {SegmentedControl} from 'sentry/components/segmentedControl';
  28. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  29. import {TabList, Tabs} from 'sentry/components/tabs';
  30. import {IconPanel} from 'sentry/icons';
  31. import {t} from 'sentry/locale';
  32. import {space} from 'sentry/styles/space';
  33. import type {PageFilters} from 'sentry/types/core';
  34. import type {Organization} from 'sentry/types/organization';
  35. import type {Project} from 'sentry/types/project';
  36. import type {DeepPartial} from 'sentry/types/utils';
  37. import {defined} from 'sentry/utils';
  38. import {browserHistory} from 'sentry/utils/browserHistory';
  39. import type EventView from 'sentry/utils/discover/eventView';
  40. import {isAggregateField} from 'sentry/utils/discover/fields';
  41. import type {CanvasScheduler} from 'sentry/utils/profiling/canvasScheduler';
  42. import {
  43. CanvasPoolManager,
  44. useCanvasScheduler,
  45. } from 'sentry/utils/profiling/canvasScheduler';
  46. import type {FlamegraphState} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContext';
  47. import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
  48. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  49. import type {Frame} from 'sentry/utils/profiling/frame';
  50. import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
  51. import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
  52. import type {ProfilingFieldType} from 'sentry/utils/profiling/hooks/useProfileEvents';
  53. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  54. import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes';
  55. import {decodeScalar} from 'sentry/utils/queryString';
  56. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  57. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  58. import {useLocation} from 'sentry/utils/useLocation';
  59. import {useNavigate} from 'sentry/utils/useNavigate';
  60. import useOrganization from 'sentry/utils/useOrganization';
  61. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  62. import {
  63. FlamegraphProvider,
  64. useFlamegraph,
  65. } from 'sentry/views/profiling/flamegraphProvider';
  66. import {ProfilesSummaryChart} from 'sentry/views/profiling/landing/profilesSummaryChart';
  67. import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
  68. import {ProfilesTable} from 'sentry/views/profiling/profileSummary/profilesTable';
  69. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  70. import {MostRegressedProfileFunctions} from './regressedProfileFunctions';
  71. import {SlowestProfileFunctions} from './slowestProfileFunctions';
  72. const noop = () => void 0;
  73. function decodeViewOrDefault(
  74. value: string | string[] | null | undefined,
  75. defaultValue: 'flamegraph' | 'profiles'
  76. ): 'flamegraph' | 'profiles' {
  77. if (!value || Array.isArray(value)) {
  78. return defaultValue;
  79. }
  80. if (value === 'flamegraph' || value === 'profiles') {
  81. return value;
  82. }
  83. return defaultValue;
  84. }
  85. const DEFAULT_FLAMEGRAPH_PREFERENCES: DeepPartial<FlamegraphState> = {
  86. preferences: {
  87. sorting: 'alphabetical' satisfies FlamegraphState['preferences']['sorting'],
  88. },
  89. };
  90. interface ProfileSummaryHeaderProps {
  91. location: Location;
  92. onViewChange: (newView: 'flamegraph' | 'profiles') => void;
  93. organization: Organization;
  94. project: Project | null;
  95. query: string;
  96. transaction: string;
  97. view: 'flamegraph' | 'profiles';
  98. }
  99. function ProfileSummaryHeader(props: ProfileSummaryHeaderProps) {
  100. const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => {
  101. return [
  102. {
  103. type: 'landing',
  104. payload: {
  105. query: props.location.query,
  106. },
  107. },
  108. {
  109. type: 'profile summary',
  110. payload: {
  111. projectSlug: props.project?.slug ?? '',
  112. query: props.location.query,
  113. transaction: props.transaction,
  114. },
  115. },
  116. ];
  117. }, [props.location.query, props.project?.slug, props.transaction]);
  118. const transactionSummaryTarget =
  119. props.project &&
  120. props.transaction &&
  121. transactionSummaryRouteWithQuery({
  122. organization: props.organization,
  123. transaction: props.transaction,
  124. projectID: props.project.id,
  125. query: {query: props.query},
  126. });
  127. return (
  128. <ProfilingHeader>
  129. <ProfilingHeaderContent>
  130. <ProfilingBreadcrumbs
  131. organization={props.organization}
  132. trails={breadcrumbTrails}
  133. />
  134. <Layout.Title>
  135. <ProfilingTitleContainer>
  136. {props.project ? (
  137. <IdBadge
  138. hideName
  139. project={props.project}
  140. avatarSize={22}
  141. avatarProps={{hasTooltip: true, tooltip: props.project.slug}}
  142. />
  143. ) : null}
  144. {props.transaction}
  145. </ProfilingTitleContainer>
  146. </Layout.Title>
  147. </ProfilingHeaderContent>
  148. {transactionSummaryTarget && (
  149. <StyledHeaderActions>
  150. <FeedbackWidgetButton />
  151. <LinkButton to={transactionSummaryTarget} size="sm">
  152. {t('View Summary')}
  153. </LinkButton>
  154. </StyledHeaderActions>
  155. )}
  156. <Tabs onChange={props.onViewChange} value={props.view}>
  157. <TabList hideBorder>
  158. <TabList.Item key="flamegraph">{t('Flamegraph')}</TabList.Item>
  159. <TabList.Item key="profiles">{t('Sampled Profiles')}</TabList.Item>
  160. </TabList>
  161. </Tabs>
  162. </ProfilingHeader>
  163. );
  164. }
  165. const ProfilingHeader = styled(Layout.Header)`
  166. padding: ${space(1)} ${space(2)} 0 ${space(2)} !important;
  167. `;
  168. const ProfilingHeaderContent = styled(Layout.HeaderContent)`
  169. margin-bottom: ${space(1)};
  170. h1 {
  171. line-height: normal;
  172. }
  173. `;
  174. const StyledHeaderActions = styled(Layout.HeaderActions)`
  175. display: flex;
  176. flex-direction: row;
  177. gap: ${space(1)};
  178. `;
  179. const ProfilingTitleContainer = styled('div')`
  180. display: flex;
  181. align-items: center;
  182. gap: ${space(1)};
  183. font-size: ${p => p.theme.fontSizeLarge};
  184. `;
  185. interface ProfileFiltersProps {
  186. location: Location;
  187. organization: Organization;
  188. projectIds: EventView['project'];
  189. query: string;
  190. selection: PageFilters;
  191. transaction: string | undefined;
  192. }
  193. function ProfileFilters(props: ProfileFiltersProps) {
  194. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  195. (searchQuery: string) => {
  196. browserHistory.push({
  197. ...props.location,
  198. query: {
  199. ...props.location.query,
  200. query: searchQuery || undefined,
  201. cursor: undefined,
  202. },
  203. });
  204. },
  205. [props.location]
  206. );
  207. const projectIds = useMemo(() => props.projectIds.slice(), [props.projectIds]);
  208. return (
  209. <ActionBar>
  210. <PageFilterBar condensed>
  211. <EnvironmentPageFilter />
  212. <DatePageFilter />
  213. </PageFilterBar>
  214. <TransactionSearchQueryBuilder
  215. projects={projectIds}
  216. initialQuery={props.query}
  217. onSearch={handleSearch}
  218. searchSource="transaction_profiles"
  219. />
  220. </ActionBar>
  221. );
  222. }
  223. const ActionBar = styled('div')`
  224. display: grid;
  225. gap: ${space(1)};
  226. grid-template-columns: min-content auto;
  227. padding: ${space(1)} ${space(1)};
  228. background-color: ${p => p.theme.background};
  229. `;
  230. interface ProfileSummaryPageProps {
  231. location: Location;
  232. params: {
  233. projectId?: Project['slug'];
  234. };
  235. selection: PageFilters;
  236. view: 'flamegraph' | 'profile list';
  237. }
  238. function ProfileSummaryPage(props: ProfileSummaryPageProps) {
  239. const organization = useOrganization();
  240. const project = useCurrentProjectFromRouteParam();
  241. const transaction = decodeScalar(props.location.query.transaction);
  242. if (!transaction) {
  243. throw new TypeError(
  244. `Profile summary requires a transaction query params, got ${
  245. transaction?.toString() ?? transaction
  246. }`
  247. );
  248. }
  249. const rawQuery = decodeScalar(props.location?.query?.query, '');
  250. const projectIds: number[] = useMemo(() => {
  251. if (!defined(project)) {
  252. return [];
  253. }
  254. const projects = parseInt(project.id, 10);
  255. if (isNaN(projects)) {
  256. return [];
  257. }
  258. return [projects];
  259. }, [project]);
  260. const projectSlugs: string[] = useMemo(() => {
  261. return defined(project) ? [project.slug] : [];
  262. }, [project]);
  263. const query = useMemo(() => {
  264. const search = new MutableSearch(rawQuery);
  265. search.setFilterValues('transaction', [transaction]);
  266. // there are no aggregations happening on this page,
  267. // so remove any aggregate filters
  268. Object.keys(search.filters).forEach(field => {
  269. if (isAggregateField(field)) {
  270. search.removeFilter(field);
  271. }
  272. });
  273. return search.formatString();
  274. }, [rawQuery, transaction]);
  275. const {data, status} = useAggregateFlamegraphQuery({
  276. query,
  277. });
  278. const [visualization, setVisualization] = useLocalStorageState<
  279. 'flamegraph' | 'call tree'
  280. >('flamegraph-visualization', 'flamegraph');
  281. const onVisualizationChange = useCallback(
  282. (value: 'flamegraph' | 'call tree') => {
  283. setVisualization(value);
  284. },
  285. [setVisualization]
  286. );
  287. const [hideRegressions, setHideRegressions] = useLocalStorageState<boolean>(
  288. 'flamegraph-hide-regressions',
  289. false
  290. );
  291. const [frameFilter, setFrameFilter] = useLocalStorageState<
  292. 'system' | 'application' | 'all'
  293. >('flamegraph-frame-filter', 'application');
  294. const onFrameFilterChange = useCallback(
  295. (value: 'system' | 'application' | 'all') => {
  296. setFrameFilter(value);
  297. },
  298. [setFrameFilter]
  299. );
  300. const flamegraphFrameFilter: ((frame: Frame) => boolean) | undefined = useMemo(() => {
  301. if (frameFilter === 'all') {
  302. return () => true;
  303. }
  304. if (frameFilter === 'application') {
  305. return frame => frame.is_application;
  306. }
  307. return frame => !frame.is_application;
  308. }, [frameFilter]);
  309. const onResetFrameFilter = useCallback(() => {
  310. setFrameFilter('all');
  311. }, [setFrameFilter]);
  312. const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
  313. const scheduler = useCanvasScheduler(canvasPoolManager);
  314. const location = useLocation();
  315. const navigate = useNavigate();
  316. const view = useMemo(() => {
  317. return decodeViewOrDefault(location.query.view, 'flamegraph');
  318. }, [location.query.view]);
  319. const setView = useCallback(
  320. (newView: 'flamegraph' | 'profiles') => {
  321. navigate({
  322. ...location,
  323. query: {
  324. ...location.query,
  325. view: newView,
  326. },
  327. });
  328. },
  329. [location, navigate]
  330. );
  331. const onHideRegressionsClick = useCallback(() => {
  332. return setHideRegressions(!hideRegressions);
  333. }, [hideRegressions, setHideRegressions]);
  334. return (
  335. <SentryDocumentTitle
  336. title={t('Profiling \u2014 Profile Summary')}
  337. orgSlug={organization.slug}
  338. >
  339. <ProfileSummaryContainer>
  340. <PageFiltersContainer
  341. shouldForceProject={defined(project)}
  342. forceProject={project}
  343. specificProjectSlugs={projectSlugs}
  344. defaultSelection={{datetime: DEFAULT_PROFILING_DATETIME_SELECTION}}
  345. >
  346. <ProfileSummaryHeader
  347. view={view}
  348. onViewChange={setView}
  349. organization={organization}
  350. location={props.location}
  351. project={project}
  352. query={rawQuery}
  353. transaction={transaction}
  354. />
  355. <ProfileFilters
  356. projectIds={projectIds}
  357. organization={organization}
  358. location={props.location}
  359. query={rawQuery}
  360. selection={props.selection}
  361. transaction={transaction}
  362. />
  363. <ProfilesSummaryChart
  364. referrer="api.profiling.profile-summary-chart"
  365. query={query}
  366. hideCount
  367. />
  368. {view === 'profiles' ? (
  369. <ProfilesTable />
  370. ) : (
  371. <ProfileVisualizationContainer hideRegressions={hideRegressions}>
  372. <ProfileVisualization>
  373. <ProfileGroupProvider
  374. traceID=""
  375. type="flamegraph"
  376. input={data ?? null}
  377. frameFilter={flamegraphFrameFilter}
  378. >
  379. <FlamegraphStateProvider initialState={DEFAULT_FLAMEGRAPH_PREFERENCES}>
  380. <FlamegraphThemeProvider>
  381. <FlamegraphProvider>
  382. <AggregateFlamegraphContainer>
  383. <AggregateFlamegraphToolbar
  384. scheduler={scheduler}
  385. canvasPoolManager={canvasPoolManager}
  386. visualization={visualization}
  387. onVisualizationChange={onVisualizationChange}
  388. frameFilter={frameFilter}
  389. onFrameFilterChange={onFrameFilterChange}
  390. hideSystemFrames={false}
  391. setHideSystemFrames={noop}
  392. onHideRegressionsClick={onHideRegressionsClick}
  393. />
  394. {status === 'pending' ? (
  395. <RequestStateMessageContainer>
  396. <LoadingIndicator />
  397. </RequestStateMessageContainer>
  398. ) : status === 'error' ? (
  399. <RequestStateMessageContainer>
  400. {t('There was an error loading the flamegraph.')}
  401. </RequestStateMessageContainer>
  402. ) : null}
  403. {visualization === 'flamegraph' ? (
  404. <AggregateFlamegraph
  405. filter={frameFilter}
  406. canvasPoolManager={canvasPoolManager}
  407. scheduler={scheduler}
  408. status={status}
  409. onResetFilter={onResetFrameFilter}
  410. />
  411. ) : (
  412. <AggregateFlamegraphTreeTable
  413. recursion={null}
  414. expanded={false}
  415. frameFilter={frameFilter}
  416. canvasPoolManager={canvasPoolManager}
  417. />
  418. )}
  419. </AggregateFlamegraphContainer>
  420. </FlamegraphProvider>
  421. </FlamegraphThemeProvider>
  422. </FlamegraphStateProvider>
  423. </ProfileGroupProvider>
  424. </ProfileVisualization>
  425. {hideRegressions ? null : (
  426. <ProfileDigestContainer>
  427. <ProfileDigestScrollContainer>
  428. <ProfileDigest onViewChange={setView} transaction={transaction} />
  429. <MostRegressedProfileFunctions transaction={transaction} />
  430. <SlowestProfileFunctions transaction={transaction} />
  431. </ProfileDigestScrollContainer>
  432. </ProfileDigestContainer>
  433. )}
  434. </ProfileVisualizationContainer>
  435. )}
  436. </PageFiltersContainer>
  437. </ProfileSummaryContainer>
  438. </SentryDocumentTitle>
  439. );
  440. }
  441. const RequestStateMessageContainer = styled('div')`
  442. position: absolute;
  443. left: 0;
  444. right: 0;
  445. top: 0;
  446. bottom: 0;
  447. display: flex;
  448. justify-content: center;
  449. align-items: center;
  450. color: ${p => p.theme.subText};
  451. `;
  452. const AggregateFlamegraphContainer = styled('div')`
  453. display: flex;
  454. flex-direction: column;
  455. flex: 1 1 100%;
  456. height: 100%;
  457. width: 100%;
  458. position: absolute;
  459. left: 0px;
  460. top: 0px;
  461. `;
  462. interface AggregateFlamegraphToolbarProps {
  463. canvasPoolManager: CanvasPoolManager;
  464. frameFilter: 'system' | 'application' | 'all';
  465. hideSystemFrames: boolean;
  466. onFrameFilterChange: (value: 'system' | 'application' | 'all') => void;
  467. onHideRegressionsClick: () => void;
  468. onVisualizationChange: (value: 'flamegraph' | 'call tree') => void;
  469. scheduler: CanvasScheduler;
  470. setHideSystemFrames: (value: boolean) => void;
  471. visualization: 'flamegraph' | 'call tree';
  472. }
  473. function AggregateFlamegraphToolbar(props: AggregateFlamegraphToolbarProps) {
  474. const flamegraph = useFlamegraph();
  475. const flamegraphs = useMemo(() => [flamegraph], [flamegraph]);
  476. const spans = useMemo(() => [], []);
  477. const frameSelectOptions: Array<SelectOption<'system' | 'application' | 'all'>> =
  478. useMemo(() => {
  479. return [
  480. {value: 'system', label: t('System Frames')},
  481. {value: 'application', label: t('Application Frames')},
  482. {value: 'all', label: t('All Frames')},
  483. ];
  484. }, []);
  485. const onResetZoom = useCallback(() => {
  486. props.scheduler.dispatch('reset zoom');
  487. }, [props.scheduler]);
  488. const onFrameFilterChange = useCallback(
  489. (value: {value: 'application' | 'system' | 'all'}) => {
  490. props.onFrameFilterChange(value.value);
  491. },
  492. [props]
  493. );
  494. return (
  495. <AggregateFlamegraphToolbarContainer>
  496. <ViewSelectContainer>
  497. <SegmentedControl
  498. aria-label={t('View')}
  499. size="xs"
  500. value={props.visualization}
  501. onChange={props.onVisualizationChange}
  502. >
  503. <SegmentedControl.Item key="flamegraph">
  504. {t('Flamegraph')}
  505. </SegmentedControl.Item>
  506. <SegmentedControl.Item key="call tree">{t('Call Tree')}</SegmentedControl.Item>
  507. </SegmentedControl>
  508. </ViewSelectContainer>
  509. <AggregateFlamegraphSearch
  510. spans={spans}
  511. canvasPoolManager={props.canvasPoolManager}
  512. flamegraphs={flamegraphs}
  513. />
  514. <Button size="xs" onClick={onResetZoom}>
  515. {t('Reset Zoom')}
  516. </Button>
  517. <CompactSelect
  518. onChange={onFrameFilterChange}
  519. value={props.frameFilter}
  520. size="xs"
  521. options={frameSelectOptions}
  522. />
  523. <Button
  524. size="xs"
  525. onClick={props.onHideRegressionsClick}
  526. title={t('Expand or collapse the view')}
  527. >
  528. <IconPanel size="xs" direction="right" />
  529. </Button>
  530. </AggregateFlamegraphToolbarContainer>
  531. );
  532. }
  533. const ViewSelectContainer = styled('div')`
  534. min-width: 160px;
  535. `;
  536. const AggregateFlamegraphToolbarContainer = styled('div')`
  537. display: flex;
  538. justify-content: space-between;
  539. gap: ${space(1)};
  540. padding: ${space(1)} ${space(0.5)};
  541. background-color: ${p => p.theme.background};
  542. /*
  543. force height to be the same as profile digest header,
  544. but subtract 1px for the border that doesnt exist on the header
  545. */
  546. height: 41px;
  547. `;
  548. const AggregateFlamegraphSearch = styled(FlamegraphSearch)`
  549. max-width: 300px;
  550. `;
  551. const ProfileVisualization = styled('div')`
  552. grid-area: visualization;
  553. position: relative;
  554. height: 100%;
  555. `;
  556. const ProfileDigestContainer = styled('div')`
  557. grid-area: digest;
  558. border-left: 1px solid ${p => p.theme.border};
  559. background-color: ${p => p.theme.background};
  560. display: flex;
  561. flex: 1 1 100%;
  562. flex-direction: column;
  563. position: relative;
  564. overflow: hidden;
  565. `;
  566. const ProfileDigestScrollContainer = styled('div')`
  567. position: absolute;
  568. left: 0;
  569. right: 0;
  570. top: 0;
  571. bottom: 0;
  572. display: flex;
  573. flex-direction: column;
  574. `;
  575. // @ts-expect-error TS(7008): Member 'hideRegressions' implicitly has an 'any' t... Remove this comment to see the full error message
  576. const ProfileVisualizationContainer = styled('div')<{hideRegressions}>`
  577. display: grid;
  578. /* false positive for grid layout */
  579. /* stylelint-disable */
  580. grid-template-areas: ${p =>
  581. p.hideRegressions ? "'visualization'" : "'visualization digest'"};
  582. grid-template-columns: ${p => (p.hideRegressions ? `100%` : `60% 40%`)};
  583. flex: 1 1 100%;
  584. `;
  585. const ProfileSummaryContainer = styled('div')`
  586. display: flex;
  587. flex-direction: column;
  588. flex: 1 1 100%;
  589. /*
  590. * The footer component is a sibling of this div.
  591. * Remove it so the flamegraph can take up the
  592. * entire screen.
  593. */
  594. ~ footer {
  595. display: none;
  596. }
  597. `;
  598. const PROFILE_DIGEST_FIELDS = [
  599. 'last_seen()',
  600. 'p75()',
  601. 'p95()',
  602. 'p99()',
  603. 'count()',
  604. ] satisfies ProfilingFieldType[];
  605. const percentiles = ['p75()', 'p95()', 'p99()'] as const;
  606. interface ProfileDigestProps {
  607. onViewChange: (newView: 'flamegraph' | 'profiles') => void;
  608. transaction: string;
  609. }
  610. function ProfileDigest(props: ProfileDigestProps) {
  611. const location = useLocation();
  612. const organization = useOrganization();
  613. const project = useCurrentProjectFromRouteParam();
  614. const query = useMemo(() => {
  615. const conditions = new MutableSearch('');
  616. conditions.setFilterValues('transaction', [props.transaction]);
  617. return conditions.formatString();
  618. }, [props.transaction]);
  619. const profilesCursor = useMemo(
  620. () => decodeScalar(location.query.cursor),
  621. [location.query.cursor]
  622. );
  623. const profiles = useProfileEvents<ProfilingFieldType>({
  624. cursor: profilesCursor,
  625. fields: PROFILE_DIGEST_FIELDS,
  626. query,
  627. sort: {key: 'last_seen()', order: 'desc'},
  628. referrer: 'api.profiling.profile-summary-table',
  629. });
  630. const data = profiles.data?.data?.[0];
  631. const latestProfile = useProfileEvents<ProfilingFieldType>({
  632. cursor: profilesCursor,
  633. fields: ['profile.id', 'timestamp'],
  634. query: '',
  635. sort: {key: 'timestamp', order: 'desc'},
  636. limit: 1,
  637. referrer: 'api.profiling.profile-summary-table',
  638. });
  639. const profile = latestProfile.data?.data?.[0];
  640. const flamegraphTarget =
  641. project && profile
  642. ? generateProfileFlamechartRoute({
  643. orgSlug: organization.slug,
  644. projectSlug: project.slug,
  645. profileId: profile?.['profile.id'] as string,
  646. })
  647. : undefined;
  648. return (
  649. <ProfileDigestHeader>
  650. <div>
  651. <ProfileDigestLabel>{t('Last Seen')}</ProfileDigestLabel>
  652. <div>
  653. {profiles.isPending ? (
  654. ''
  655. ) : profiles.isError ? (
  656. ''
  657. ) : flamegraphTarget ? (
  658. <Link to={flamegraphTarget}>
  659. <DateTime date={new Date(data?.['last_seen()'] as string)} />
  660. </Link>
  661. ) : (
  662. <DateTime date={new Date(data?.['last_seen()'] as string)} />
  663. )}
  664. </div>
  665. </div>
  666. {percentiles.map(p => {
  667. return (
  668. <ProfileDigestColumn key={p}>
  669. <ProfileDigestLabel>{p}</ProfileDigestLabel>
  670. <div>
  671. {profiles.isPending ? (
  672. ''
  673. ) : profiles.isError ? (
  674. ''
  675. ) : (
  676. <PerformanceDuration nanoseconds={data?.[p] as number} abbreviation />
  677. )}
  678. </div>
  679. </ProfileDigestColumn>
  680. );
  681. })}
  682. <ProfileDigestColumn>
  683. <ProfileDigestLabel>{t('profiles')}</ProfileDigestLabel>
  684. <div>
  685. {profiles.isPending ? (
  686. ''
  687. ) : profiles.isError ? (
  688. ''
  689. ) : (
  690. <Count value={data?.['count()'] as number} />
  691. )}
  692. </div>
  693. </ProfileDigestColumn>
  694. </ProfileDigestHeader>
  695. );
  696. }
  697. const ProfileDigestColumn = styled('div')`
  698. text-align: right;
  699. `;
  700. const ProfileDigestHeader = styled('div')`
  701. display: flex;
  702. justify-content: space-between;
  703. align-items: center;
  704. padding: 0 ${space(1)};
  705. border-bottom: 1px solid ${p => p.theme.border};
  706. /* force height to be same as toolbar */
  707. height: 42px;
  708. flex-shrink: 0;
  709. `;
  710. const ProfileDigestLabel = styled('span')`
  711. color: ${p => p.theme.textColor};
  712. font-size: ${p => p.theme.fontSizeSmall};
  713. font-weight: ${p => p.theme.fontWeightBold};
  714. text-transform: uppercase;
  715. `;
  716. export default function ProfileSummaryPageToggle(props: ProfileSummaryPageProps) {
  717. return (
  718. <ProfileSummaryContainer data-test-id="profile-summary-redesign">
  719. <ErrorBoundary>
  720. <ProfileSummaryPage {...props} />
  721. </ErrorBoundary>
  722. </ProfileSummaryContainer>
  723. );
  724. }