content.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. import {Fragment, useCallback, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import {Alert} from 'sentry/components/alert';
  5. import {Button, LinkButton} from 'sentry/components/button';
  6. import SearchBar from 'sentry/components/events/searchBar';
  7. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  8. import * as Layout from 'sentry/components/layouts/thirds';
  9. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  10. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  11. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  12. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  13. import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
  14. import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
  15. import Pagination from 'sentry/components/pagination';
  16. import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder';
  17. import {
  18. ProfilingAM1OrMMXUpgrade,
  19. ProfilingBetaAlertBanner,
  20. ProfilingUpgradeButton,
  21. } from 'sentry/components/profiling/billing/alerts';
  22. import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
  23. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  24. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  25. import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
  26. import {TabList, Tabs} from 'sentry/components/tabs';
  27. import {MAX_QUERY_LENGTH} from 'sentry/constants';
  28. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  29. import {t} from 'sentry/locale';
  30. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  31. import {space} from 'sentry/styles/space';
  32. import {trackAnalytics} from 'sentry/utils/analytics';
  33. import {browserHistory} from 'sentry/utils/browserHistory';
  34. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  35. import {formatError, formatSort} from 'sentry/utils/profiling/hooks/utils';
  36. import {decodeScalar} from 'sentry/utils/queryString';
  37. import {useLocation} from 'sentry/utils/useLocation';
  38. import useOrganization from 'sentry/utils/useOrganization';
  39. import usePageFilters from 'sentry/utils/usePageFilters';
  40. import useProjects from 'sentry/utils/useProjects';
  41. import {LandingAggregateFlamegraph} from 'sentry/views/profiling/landingAggregateFlamegraph';
  42. import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
  43. import {LandingWidgetSelector} from './landing/landingWidgetSelector';
  44. import {ProfilesChart} from './landing/profileCharts';
  45. import {ProfilesChartWidget} from './landing/profilesChartWidget';
  46. import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
  47. import {SlowestFunctionsTable} from './landing/slowestFunctionsTable';
  48. import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
  49. const LEFT_WIDGET_CURSOR = 'leftCursor';
  50. const RIGHT_WIDGET_CURSOR = 'rightCursor';
  51. const CURSOR_PARAMS = [LEFT_WIDGET_CURSOR, RIGHT_WIDGET_CURSOR];
  52. interface ProfilingContentProps {
  53. location: Location;
  54. }
  55. function ProfilingContentLegacy({location}: ProfilingContentProps) {
  56. const organization = useOrganization();
  57. const {selection} = usePageFilters();
  58. const cursor = decodeScalar(location.query.cursor);
  59. const query = decodeScalar(location.query.query, '');
  60. const fields = ALL_FIELDS;
  61. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  62. key: 'count()',
  63. order: 'desc',
  64. });
  65. const {projects} = useProjects();
  66. const transactions = useProfileEvents<FieldType>({
  67. cursor,
  68. fields,
  69. query,
  70. sort,
  71. referrer: 'api.profiling.landing-table',
  72. });
  73. const transactionsError =
  74. transactions.status === 'error' ? formatError(transactions.error) : null;
  75. useEffect(() => {
  76. trackAnalytics('profiling_views.landing', {
  77. organization,
  78. });
  79. }, [organization]);
  80. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  81. (searchQuery: string) => {
  82. browserHistory.push({
  83. ...location,
  84. query: {
  85. ...location.query,
  86. cursor: undefined,
  87. query: searchQuery || undefined,
  88. },
  89. });
  90. },
  91. [location]
  92. );
  93. // Open the modal on demand
  94. const onSetupProfilingClick = useCallback(() => {
  95. trackAnalytics('profiling_views.onboarding', {
  96. organization,
  97. });
  98. SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING);
  99. }, [organization]);
  100. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  101. // if it's My Projects or All projects, only show onboarding if we can't
  102. // find any projects with profiles
  103. if (
  104. selection.projects.length === 0 ||
  105. selection.projects[0] === ALL_ACCESS_PROJECTS
  106. ) {
  107. return projects.every(project => !project.hasProfiles);
  108. }
  109. // otherwise, only show onboarding if we can't find any projects with profiles
  110. // from those that were selected
  111. const projectsWithProfiles = new Set(
  112. projects.filter(project => project.hasProfiles).map(project => project.id)
  113. );
  114. return selection.projects.every(
  115. project => !projectsWithProfiles.has(String(project))
  116. );
  117. }, [selection.projects, projects]);
  118. return (
  119. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  120. <PageFiltersContainer
  121. defaultSelection={{datetime: DEFAULT_PROFILING_DATETIME_SELECTION}}
  122. >
  123. <Layout.Page>
  124. <ProfilingBetaAlertBanner organization={organization} />
  125. <Layout.Header>
  126. <StyledHeaderContent>
  127. <Layout.Title>
  128. {t('Profiling')}
  129. <PageHeadingQuestionTooltip
  130. docsUrl="https://docs.sentry.io/product/profiling/"
  131. title={t(
  132. 'Profiling collects detailed information in production about the functions executing in your application and how long they take to run, giving you code-level visibility into your hot paths.'
  133. )}
  134. />
  135. </Layout.Title>
  136. <FeedbackWidgetButton />
  137. </StyledHeaderContent>
  138. </Layout.Header>
  139. <Layout.Body>
  140. <Layout.Main fullWidth>
  141. {transactionsError && (
  142. <Alert type="error" showIcon>
  143. {transactionsError}
  144. </Alert>
  145. )}
  146. <ActionBar>
  147. <PageFilterBar condensed>
  148. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  149. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  150. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  151. </PageFilterBar>
  152. {organization.features.includes('search-query-builder-performance') ? (
  153. <TransactionSearchQueryBuilder
  154. projects={selection.projects}
  155. initialQuery={query}
  156. onSearch={handleSearch}
  157. searchSource="profile_landing"
  158. />
  159. ) : (
  160. <SearchBar
  161. searchSource="profile_landing"
  162. organization={organization}
  163. projectIds={selection.projects}
  164. query={query}
  165. onSearch={handleSearch}
  166. maxQueryLength={MAX_QUERY_LENGTH}
  167. />
  168. )}
  169. </ActionBar>
  170. {shouldShowProfilingOnboardingPanel ? (
  171. <Fragment>
  172. <ProfilingOnboardingPanel
  173. content={
  174. // If user is on m2, show default
  175. <ProfilingAM1OrMMXUpgrade
  176. organization={organization}
  177. fallback={
  178. <Fragment>
  179. <h3>{t('Function level insights')}</h3>
  180. <p>
  181. {t(
  182. 'Discover slow-to-execute or resource intensive functions within your application'
  183. )}
  184. </p>
  185. </Fragment>
  186. }
  187. />
  188. }
  189. >
  190. <ProfilingUpgradeButton
  191. data-test-id="profiling-upgrade"
  192. organization={organization}
  193. priority="primary"
  194. onClick={onSetupProfilingClick}
  195. fallback={
  196. <Button onClick={onSetupProfilingClick} priority="primary">
  197. {t('Set Up Profiling')}
  198. </Button>
  199. }
  200. >
  201. {t('Set Up Profiling')}
  202. </ProfilingUpgradeButton>
  203. <LinkButton href="https://docs.sentry.io/product/profiling/" external>
  204. {t('Read Docs')}
  205. </LinkButton>
  206. </ProfilingOnboardingPanel>
  207. </Fragment>
  208. ) : (
  209. <Fragment>
  210. {organization.features.includes(
  211. 'profiling-global-suspect-functions'
  212. ) ? (
  213. <Fragment>
  214. <ProfilesChartWidget
  215. chartHeight={150}
  216. referrer="api.profiling.landing-chart"
  217. userQuery={query}
  218. selection={selection}
  219. />
  220. <WidgetsContainer>
  221. <LandingWidgetSelector
  222. cursorName={LEFT_WIDGET_CURSOR}
  223. widgetHeight="340px"
  224. defaultWidget="slowest functions"
  225. query={query}
  226. storageKey="profiling-landing-widget-0"
  227. />
  228. <LandingWidgetSelector
  229. cursorName={RIGHT_WIDGET_CURSOR}
  230. widgetHeight="340px"
  231. defaultWidget="regressed functions"
  232. query={query}
  233. storageKey="profiling-landing-widget-1"
  234. />
  235. </WidgetsContainer>
  236. </Fragment>
  237. ) : (
  238. <PanelsGrid>
  239. <ProfilingSlowestTransactionsPanel />
  240. <ProfilesChart
  241. referrer="api.profiling.landing-chart"
  242. query={query}
  243. selection={selection}
  244. hideCount
  245. />
  246. </PanelsGrid>
  247. )}
  248. <ProfileEventsTable
  249. columns={fields.slice()}
  250. data={transactions.status === 'success' ? transactions.data : null}
  251. error={
  252. transactions.status === 'error'
  253. ? t('Unable to load profiles')
  254. : null
  255. }
  256. isLoading={transactions.status === 'loading'}
  257. sort={sort}
  258. sortableColumns={new Set(fields)}
  259. />
  260. <Pagination
  261. pageLinks={
  262. transactions.status === 'success'
  263. ? transactions.getResponseHeader?.('Link') ?? null
  264. : null
  265. }
  266. />
  267. </Fragment>
  268. )}
  269. </Layout.Main>
  270. </Layout.Body>
  271. </Layout.Page>
  272. </PageFiltersContainer>
  273. </SentryDocumentTitle>
  274. );
  275. }
  276. function validateTab(tab: unknown): tab is 'flamegraph' | 'transactions' {
  277. return tab === 'flamegraph' || tab === 'transactions';
  278. }
  279. function decodeTab(tab: unknown): 'flamegraph' | 'transactions' {
  280. // Fallback to transactions if tab is invalid. We default to transactions
  281. // because that is going to be the most common perf setup when we release.
  282. return validateTab(tab) ? tab : 'transactions';
  283. }
  284. function ProfilingContent({location}: ProfilingContentProps) {
  285. const organization = useOrganization();
  286. const {selection} = usePageFilters();
  287. const {projects} = useProjects();
  288. const tab = decodeTab(location.query.tab);
  289. useEffect(() => {
  290. trackAnalytics('profiling_views.landing', {
  291. organization,
  292. });
  293. }, [organization]);
  294. const onTabChange = useCallback(
  295. (newTab: 'flamegraph' | 'transactions') => {
  296. browserHistory.push({
  297. ...location,
  298. query: {
  299. ...location.query,
  300. tab: newTab,
  301. },
  302. });
  303. },
  304. [location]
  305. );
  306. const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
  307. // if it's My Projects or All projects, only show onboarding if we can't
  308. // find any projects with profiles
  309. if (
  310. selection.projects.length === 0 ||
  311. selection.projects[0] === ALL_ACCESS_PROJECTS
  312. ) {
  313. return projects.every(project => !project.hasProfiles);
  314. }
  315. // otherwise, only show onboarding if we can't find any projects with profiles
  316. // from those that were selected
  317. const projectsWithProfiles = new Set(
  318. projects.filter(project => project.hasProfiles).map(project => project.id)
  319. );
  320. return selection.projects.every(
  321. project => !projectsWithProfiles.has(String(project))
  322. );
  323. }, [selection.projects, projects]);
  324. return (
  325. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  326. <PageFiltersContainer
  327. defaultSelection={{datetime: DEFAULT_PROFILING_DATETIME_SELECTION}}
  328. >
  329. <Layout.Page>
  330. <ProfilingBetaAlertBanner organization={organization} />
  331. <ProfilingContentPageHeader tab={tab} onTabChange={onTabChange} />
  332. <Layout.Body>
  333. {tab === 'flamegraph' ? (
  334. <ProfilingFlamegraphTabContent
  335. tab={tab}
  336. shouldShowProfilingOnboardingPanel={shouldShowProfilingOnboardingPanel}
  337. />
  338. ) : tab === 'transactions' ? (
  339. <ProfilingTransactionsContent
  340. tab={tab}
  341. shouldShowProfilingOnboardingPanel={shouldShowProfilingOnboardingPanel}
  342. />
  343. ) : null}
  344. </Layout.Body>
  345. </Layout.Page>
  346. </PageFiltersContainer>
  347. </SentryDocumentTitle>
  348. );
  349. }
  350. interface ProfilingTabContentProps {
  351. shouldShowProfilingOnboardingPanel: boolean;
  352. tab: 'flamegraph' | 'transactions';
  353. }
  354. function ProfilingFlamegraphTabContent(props: ProfilingTabContentProps) {
  355. return (
  356. <Layout.Main fullWidth>
  357. <ActionBar>
  358. <PageFilterBar condensed>
  359. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  360. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  361. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  362. </PageFilterBar>
  363. </ActionBar>
  364. {props.shouldShowProfilingOnboardingPanel ? (
  365. <ProfilingOnboardingCTA />
  366. ) : (
  367. <LandingAggregateFlamegraphContainer>
  368. <LandingAggregateFlamegraph />
  369. </LandingAggregateFlamegraphContainer>
  370. )}
  371. </Layout.Main>
  372. );
  373. }
  374. function ProfilingTransactionsContent(props: ProfilingTabContentProps) {
  375. const organization = useOrganization();
  376. const location = useLocation();
  377. const {selection} = usePageFilters();
  378. const fields = ALL_FIELDS;
  379. const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
  380. key: 'count()',
  381. order: 'desc',
  382. });
  383. const cursor = decodeScalar(location.query.cursor);
  384. const query = decodeScalar(location.query.query, '');
  385. const continuousProfilingCompat = organization.features.includes(
  386. 'continuous-profiling-compat'
  387. );
  388. const transactions = useProfileEvents<FieldType>({
  389. cursor,
  390. fields,
  391. query,
  392. sort,
  393. referrer: 'api.profiling.landing-table',
  394. continuousProfilingCompat,
  395. });
  396. const transactionsError =
  397. transactions.status === 'error' ? formatError(transactions.error) : null;
  398. const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
  399. (searchQuery: string) => {
  400. browserHistory.push({
  401. ...location,
  402. query: {
  403. ...location.query,
  404. cursor: undefined,
  405. query: searchQuery || undefined,
  406. },
  407. });
  408. },
  409. [location]
  410. );
  411. return (
  412. <Layout.Main fullWidth>
  413. {transactionsError && (
  414. <Alert type="error" showIcon>
  415. {transactionsError}
  416. </Alert>
  417. )}
  418. <ActionBar>
  419. <PageFilterBar condensed>
  420. <ProjectPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  421. <EnvironmentPageFilter resetParamsOnChange={CURSOR_PARAMS} />
  422. <DatePageFilter resetParamsOnChange={CURSOR_PARAMS} />
  423. </PageFilterBar>
  424. {organization.features.includes('search-query-builder-performance') ? (
  425. <TransactionSearchQueryBuilder
  426. projects={selection.projects}
  427. initialQuery={query}
  428. onSearch={handleSearch}
  429. searchSource="profile_landing"
  430. />
  431. ) : (
  432. <SearchBar
  433. searchSource="profile_landing"
  434. organization={organization}
  435. projectIds={selection.projects}
  436. query={query}
  437. onSearch={handleSearch}
  438. maxQueryLength={MAX_QUERY_LENGTH}
  439. />
  440. )}
  441. </ActionBar>
  442. {props.shouldShowProfilingOnboardingPanel ? (
  443. <ProfilingOnboardingCTA />
  444. ) : (
  445. <Fragment>
  446. {organization.features.includes('continuous-profiling-compat') ? (
  447. <Fragment>
  448. <ProfilesChartWidget
  449. chartHeight={150}
  450. referrer="api.profiling.landing-chart"
  451. userQuery={query}
  452. selection={selection}
  453. continuousProfilingCompat={continuousProfilingCompat}
  454. />
  455. <SlowestFunctionsTable />
  456. </Fragment>
  457. ) : organization.features.includes('profiling-global-suspect-functions') ? (
  458. <Fragment>
  459. <ProfilesChartWidget
  460. chartHeight={150}
  461. referrer="api.profiling.landing-chart"
  462. userQuery={query}
  463. selection={selection}
  464. continuousProfilingCompat={continuousProfilingCompat}
  465. />
  466. <WidgetsContainer>
  467. <LandingWidgetSelector
  468. cursorName={LEFT_WIDGET_CURSOR}
  469. widgetHeight="340px"
  470. defaultWidget="slowest functions"
  471. query={query}
  472. storageKey="profiling-landing-widget-0"
  473. />
  474. <LandingWidgetSelector
  475. cursorName={RIGHT_WIDGET_CURSOR}
  476. widgetHeight="340px"
  477. defaultWidget="regressed functions"
  478. query={query}
  479. storageKey="profiling-landing-widget-1"
  480. />
  481. </WidgetsContainer>
  482. </Fragment>
  483. ) : (
  484. <PanelsGrid>
  485. <ProfilingSlowestTransactionsPanel />
  486. <ProfilesChart
  487. referrer="api.profiling.landing-chart"
  488. query={query}
  489. selection={selection}
  490. hideCount
  491. />
  492. </PanelsGrid>
  493. )}
  494. <Fragment>
  495. <ProfileEventsTable
  496. columns={fields.slice()}
  497. data={transactions.status === 'success' ? transactions.data : null}
  498. error={
  499. transactions.status === 'error' ? t('Unable to load profiles') : null
  500. }
  501. isLoading={transactions.status === 'loading'}
  502. sort={sort}
  503. sortableColumns={new Set(fields)}
  504. />
  505. <Pagination
  506. pageLinks={
  507. transactions.status === 'success'
  508. ? transactions.getResponseHeader?.('Link') ?? null
  509. : null
  510. }
  511. />
  512. </Fragment>
  513. </Fragment>
  514. )}
  515. </Layout.Main>
  516. );
  517. }
  518. function ProfilingOnboardingCTA() {
  519. const organization = useOrganization();
  520. // Open the modal on demand
  521. const onSetupProfilingClick = useCallback(() => {
  522. trackAnalytics('profiling_views.onboarding', {
  523. organization,
  524. });
  525. SidebarPanelStore.activatePanel(SidebarPanelKey.PROFILING_ONBOARDING);
  526. }, [organization]);
  527. return (
  528. <Fragment>
  529. <ProfilingOnboardingPanel
  530. content={
  531. // If user is on m2, show default
  532. <ProfilingAM1OrMMXUpgrade
  533. organization={organization}
  534. fallback={
  535. <Fragment>
  536. <h3>{t('Function level insights')}</h3>
  537. <p>
  538. {t(
  539. 'Discover slow-to-execute or resource intensive functions within your application'
  540. )}
  541. </p>
  542. </Fragment>
  543. }
  544. />
  545. }
  546. >
  547. <ProfilingUpgradeButton
  548. data-test-id="profiling-upgrade"
  549. organization={organization}
  550. priority="primary"
  551. onClick={onSetupProfilingClick}
  552. fallback={
  553. <Button onClick={onSetupProfilingClick} priority="primary">
  554. {t('Set Up Profiling')}
  555. </Button>
  556. }
  557. >
  558. {t('Set Up Profiling')}
  559. </ProfilingUpgradeButton>
  560. <LinkButton href="https://docs.sentry.io/product/profiling/" external>
  561. {t('Read Docs')}
  562. </LinkButton>
  563. </ProfilingOnboardingPanel>
  564. </Fragment>
  565. );
  566. }
  567. interface ProfilingContentPageHeaderProps {
  568. onTabChange: (newTab: 'flamegraph' | 'transactions') => void;
  569. tab: 'flamegraph' | 'transactions';
  570. }
  571. function ProfilingContentPageHeader(props: ProfilingContentPageHeaderProps) {
  572. return (
  573. <StyledLayoutHeader>
  574. <StyledHeaderContent>
  575. <Layout.Title>
  576. {t('Profiling')}
  577. <PageHeadingQuestionTooltip
  578. docsUrl="https://docs.sentry.io/product/profiling/"
  579. title={t(
  580. 'Profiling collects detailed information in production about the functions executing in your application and how long they take to run, giving you code-level visibility into your hot paths.'
  581. )}
  582. />
  583. </Layout.Title>
  584. <FeedbackWidgetButton />
  585. </StyledHeaderContent>
  586. <div>
  587. <Tabs value={props.tab} onChange={props.onTabChange}>
  588. <TabList hideBorder>
  589. <TabList.Item key="transactions">{t('Transactions')}</TabList.Item>
  590. <TabList.Item key="flamegraph">{t('Flamegraph')}</TabList.Item>
  591. </TabList>
  592. </Tabs>
  593. </div>
  594. </StyledLayoutHeader>
  595. );
  596. }
  597. const ALL_FIELDS = [
  598. 'transaction',
  599. 'project.id',
  600. 'last_seen()',
  601. 'p50()',
  602. 'p75()',
  603. 'p95()',
  604. 'p99()',
  605. 'count()',
  606. ] as const;
  607. type FieldType = (typeof ALL_FIELDS)[number];
  608. const LandingAggregateFlamegraphContainer = styled('div')`
  609. height: 40vh;
  610. min-height: 300px;
  611. position: relative;
  612. border: 1px solid ${p => p.theme.border};
  613. border-radius: ${p => p.theme.borderRadius};
  614. margin-bottom: ${space(2)};
  615. `;
  616. const StyledLayoutHeader = styled(Layout.Header)`
  617. display: block;
  618. `;
  619. const StyledHeaderContent = styled(Layout.HeaderContent)`
  620. display: flex;
  621. align-items: center;
  622. justify-content: space-between;
  623. flex-direction: row;
  624. `;
  625. const ActionBar = styled('div')`
  626. display: grid;
  627. gap: ${space(2)};
  628. grid-template-columns: min-content auto;
  629. margin-bottom: ${space(2)};
  630. `;
  631. // TODO: another simple primitive that can easily be <Grid columns={2} />
  632. const PanelsGrid = styled('div')`
  633. display: grid;
  634. grid-template-columns: minmax(0, 1fr) 1fr;
  635. gap: ${space(2)};
  636. @media (max-width: ${p => p.theme.breakpoints.small}) {
  637. grid-template-columns: minmax(0, 1fr);
  638. }
  639. `;
  640. const WidgetsContainer = styled('div')`
  641. display: grid;
  642. grid-template-columns: 1fr 1fr;
  643. gap: ${space(2)};
  644. @media (max-width: ${p => p.theme.breakpoints.small}) {
  645. grid-template-columns: 1fr;
  646. }
  647. `;
  648. function ProfilingContentWrapper(props: ProfilingContentProps) {
  649. const organization = useOrganization();
  650. if (organization.features.includes('continuous-profiling-compat')) {
  651. return <ProfilingContent {...props} />;
  652. }
  653. return <ProfilingContentLegacy {...props} />;
  654. }
  655. export default ProfilingContentWrapper;