123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- import {Fragment, useCallback, useEffect, useMemo} from 'react';
- import {browserHistory} from 'react-router';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import {Alert} from 'sentry/components/alert';
- import {Button} from 'sentry/components/button';
- import DatePageFilter from 'sentry/components/datePageFilter';
- import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
- import SearchBar from 'sentry/components/events/searchBar';
- import FeatureBadge from 'sentry/components/featureBadge';
- import * as Layout from 'sentry/components/layouts/thirds';
- import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
- import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
- import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
- import Pagination from 'sentry/components/pagination';
- import {
- ProfilingAM1OrMMXUpgrade,
- ProfilingBetaAlertBanner,
- ProfilingUpgradeButton,
- } from 'sentry/components/profiling/billing/alerts';
- import {ProfileEventsTable} from 'sentry/components/profiling/profileEventsTable';
- import ProjectPageFilter from 'sentry/components/projectPageFilter';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import {SidebarPanelKey} from 'sentry/components/sidebar/types';
- import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
- import {MAX_QUERY_LENGTH} from 'sentry/constants';
- import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
- import {t} from 'sentry/locale';
- import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
- import {space} from 'sentry/styles/space';
- import {Organization} from 'sentry/types';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import EventView from 'sentry/utils/discover/eventView';
- import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
- import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
- import {formatError, formatSort} from 'sentry/utils/profiling/hooks/utils';
- import {decodeScalar} from 'sentry/utils/queryString';
- import useOrganization from 'sentry/utils/useOrganization';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import useProjects from 'sentry/utils/useProjects';
- import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
- import {ProfileCharts} from './landing/profileCharts';
- import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
- import {SlowestFunctionsWidget} from './landing/slowestFunctionsWidget';
- import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
- interface ProfilingContentProps {
- location: Location;
- }
- function ProfilingContent({location}: ProfilingContentProps) {
- const organization = useOrganization();
- const {selection} = usePageFilters();
- const cursor = decodeScalar(location.query.cursor);
- const query = decodeScalar(location.query.query, '');
- const profilingUsingTransactions = organization.features.includes(
- 'profiling-using-transactions'
- );
- const fields = profilingUsingTransactions ? ALL_FIELDS : BASE_FIELDS;
- const sort = formatSort<FieldType>(decodeScalar(location.query.sort), fields, {
- key: 'p95()',
- order: 'desc',
- });
- const profileFilters = useProfileFilters({
- query: '',
- selection,
- disabled: profilingUsingTransactions,
- });
- const {projects} = useProjects();
- const transactions = useProfileEvents<FieldType>({
- cursor,
- fields,
- query,
- sort,
- referrer: 'api.profiling.landing-table',
- });
- const transactionsError =
- transactions.status === 'error' ? formatError(transactions.error) : null;
- useEffect(() => {
- trackAnalytics('profiling_views.landing', {
- organization,
- });
- }, [organization]);
- const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
- (searchQuery: string) => {
- browserHistory.push({
- ...location,
- query: {
- ...location.query,
- cursor: undefined,
- query: searchQuery || undefined,
- },
- });
- },
- [location]
- );
- // Open the modal on demand
- const onSetupProfilingClick = useCallback(() => {
- trackAnalytics('profiling_views.onboarding', {
- organization,
- });
- SidebarPanelStore.activatePanel(SidebarPanelKey.ProfilingOnboarding);
- }, [organization]);
- const shouldShowProfilingOnboardingPanel = useMemo((): boolean => {
- // if it's My Projects or All projects, only show onboarding if we can't
- // find any projects with profiles
- if (
- selection.projects.length === 0 ||
- selection.projects[0] === ALL_ACCESS_PROJECTS
- ) {
- return projects.every(project => !project.hasProfiles);
- }
- // otherwise, only show onboarding if we can't find any projects with profiles
- // from those that were selected
- const projectsWithProfiles = new Set(
- projects.filter(project => project.hasProfiles).map(project => project.id)
- );
- return selection.projects.every(
- project => !projectsWithProfiles.has(String(project))
- );
- }, [selection.projects, projects]);
- const eventView = useMemo(() => {
- const _eventView = EventView.fromNewQueryWithLocation(
- {
- id: undefined,
- version: 2,
- name: t('Profiling'),
- fields: [],
- query,
- projects: selection.projects,
- },
- location
- );
- _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
- return _eventView;
- }, [location, query, selection.projects]);
- const isProfilingGA = organization.features.includes('profiling-ga');
- return (
- <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
- <PageFiltersContainer
- defaultSelection={
- profilingUsingTransactions
- ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
- : undefined
- }
- >
- <Layout.Page>
- {isProfilingGA ? (
- <ProfilingBetaAlertBanner organization={organization} />
- ) : (
- <ProfilingBetaEndAlertBanner organization={organization} />
- )}
- <Layout.Header>
- <Layout.HeaderContent>
- <Layout.Title>
- {t('Profiling')}
- <PageHeadingQuestionTooltip
- docsUrl="https://docs.sentry.io/product/profiling/"
- title={t(
- '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.'
- )}
- />
- {isProfilingGA ? (
- <FeatureBadge type="new" />
- ) : (
- <FeatureBadge type="beta" />
- )}
- </Layout.Title>
- </Layout.HeaderContent>
- </Layout.Header>
- <Layout.Body>
- <Layout.Main fullWidth>
- {transactionsError && (
- <Alert type="error" showIcon>
- {transactionsError}
- </Alert>
- )}
- <ActionBar>
- <PageFilterBar condensed>
- <ProjectPageFilter />
- <EnvironmentPageFilter />
- <DatePageFilter alignDropdown="left" />
- </PageFilterBar>
- {profilingUsingTransactions ? (
- <SearchBar
- searchSource="profile_summary"
- organization={organization}
- projectIds={eventView.project}
- query={query}
- onSearch={handleSearch}
- maxQueryLength={MAX_QUERY_LENGTH}
- />
- ) : (
- <SmartSearchBar
- organization={organization}
- hasRecentSearches
- searchSource="profile_landing"
- supportedTags={profileFilters}
- query={query}
- onSearch={handleSearch}
- maxQueryLength={MAX_QUERY_LENGTH}
- />
- )}
- </ActionBar>
- {shouldShowProfilingOnboardingPanel ? (
- isProfilingGA ? (
- // If user is on m2, show default
- <ProfilingOnboardingPanel
- content={
- <ProfilingAM1OrMMXUpgrade
- organization={organization}
- fallback={
- <Fragment>
- <h3>{t('Function level insights')}</h3>
- <p>
- {t(
- 'Discover slow-to-execute or resource intensive functions within your application'
- )}
- </p>
- </Fragment>
- }
- />
- }
- >
- <ProfilingUpgradeButton
- organization={organization}
- priority="primary"
- fallback={
- <Button onClick={onSetupProfilingClick} priority="primary">
- {t('Set Up Profiling')}
- </Button>
- }
- >
- {t('Update plan')}
- </ProfilingUpgradeButton>
- <Button href="https://docs.sentry.io/product/profiling/" external>
- {t('Read Docs')}
- </Button>
- </ProfilingOnboardingPanel>
- ) : (
- // show previous state
- <ProfilingOnboardingPanel>
- <Button onClick={onSetupProfilingClick} priority="primary">
- {t('Set Up Profiling')}
- </Button>
- <Button href="https://docs.sentry.io/product/profiling/" external>
- {t('Read Docs')}
- </Button>
- </ProfilingOnboardingPanel>
- )
- ) : (
- <Fragment>
- <PanelsGrid>
- {organization.features.includes(
- 'profiling-global-suspect-functions'
- ) ? (
- <SlowestFunctionsWidget />
- ) : (
- <ProfilingSlowestTransactionsPanel />
- )}
- <ProfileCharts
- referrer="api.profiling.landing-chart"
- query={query}
- selection={selection}
- hideCount
- />
- </PanelsGrid>
- <ProfileEventsTable
- columns={fields.slice()}
- data={transactions.status === 'success' ? transactions.data : null}
- error={
- transactions.status === 'error'
- ? t('Unable to load profiles')
- : null
- }
- isLoading={transactions.status === 'loading'}
- sort={sort}
- sortableColumns={new Set(fields)}
- />
- <Pagination
- pageLinks={
- transactions.status === 'success'
- ? transactions.getResponseHeader?.('Link') ?? null
- : null
- }
- />
- </Fragment>
- )}
- </Layout.Main>
- </Layout.Body>
- </Layout.Page>
- </PageFiltersContainer>
- </SentryDocumentTitle>
- );
- }
- function ProfilingBetaEndAlertBanner({organization}: {organization: Organization}) {
- // beta users will continue to have access
- if (organization.features.includes('profiling-beta')) {
- return null;
- }
- return (
- <StyledAlert system type="info">
- {t(
- "The beta program for Profiling is now closed, but Profiling will become generally available soon. If you weren't part of the beta program, any Profiles sent during this time won't appear in your dashboard. Check out the What’s New tab for updates."
- )}
- </StyledAlert>
- );
- }
- const BASE_FIELDS = [
- 'transaction',
- 'project.id',
- 'last_seen()',
- 'p75()',
- 'p95()',
- 'p99()',
- 'count()',
- ] as const;
- // user misery is only available with the profiling-using-transactions feature
- const ALL_FIELDS = [...BASE_FIELDS, 'user_misery()'] as const;
- type FieldType = (typeof ALL_FIELDS)[number];
- const ActionBar = styled('div')`
- display: grid;
- gap: ${space(2)};
- grid-template-columns: min-content auto;
- margin-bottom: ${space(2)};
- `;
- // TODO: another simple primitive that can easily be <Grid columns={2} />
- const PanelsGrid = styled('div')`
- display: grid;
- grid-template-columns: minmax(0, 1fr) 1fr;
- gap: ${space(2)};
- @media (max-width: ${p => p.theme.breakpoints.small}) {
- grid-template-columns: minmax(0, 1fr);
- }
- `;
- const StyledAlert = styled(Alert)`
- margin: 0;
- `;
- export default ProfilingContent;
|