content.tsx 25 KB

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