import {Fragment, useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {RouteComponentProps} from 'react-router';
import styled from '@emotion/styled';
import omit from 'lodash/omit';
import {loadDocs, removeProject} from 'sentry/actionCreators/projects';
import Feature from 'sentry/components/acl/feature';
import {Alert} from 'sentry/components/alert';
import {Button} from 'sentry/components/button';
import ButtonBar from 'sentry/components/buttonBar';
import Confirm from 'sentry/components/confirm';
import NotFound from 'sentry/components/errors/notFound';
import HookOrDefault from 'sentry/components/hookOrDefault';
import ExternalLink from 'sentry/components/links/externalLink';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper';
import {Footer} from 'sentry/components/onboarding/footer';
import {
migratedDocs,
SdkDocumentation,
} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
import {ProductSolution} from 'sentry/components/onboarding/productSelection';
import {useRecentCreatedProject} from 'sentry/components/onboarding/useRecentCreatedProject';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {
performance as performancePlatforms,
Platform,
PlatformKey,
} from 'sentry/data/platformCategories';
import platforms from 'sentry/data/platforms';
import {IconChevron} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import ConfigStore from 'sentry/stores/configStore';
import {space} from 'sentry/styles/space';
import {OnboardingSelectedSDK, Organization, Project} from 'sentry/types';
import {IssueAlertRule} from 'sentry/types/alerts';
import {trackAnalytics} from 'sentry/utils/analytics';
import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
import {useApiQuery} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import {SetupDocsLoader} from 'sentry/views/onboarding/setupDocsLoader';
import {GettingStartedWithProjectContext} from 'sentry/views/projects/gettingStartedWithProjectContext';
const ProductUnavailableCTAHook = HookOrDefault({
hookName: 'component:product-unavailable-cta',
});
type Props = RouteComponentProps<{projectId: string}, {}>;
export function SetUpGeneralSdkDoc({
organization,
projectSlug,
platform,
}: {
organization: Organization;
platform: Platform;
projectSlug: Project['slug'];
}) {
const api = useApi();
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [html, setHtml] = useState('');
const fetchDocs = useCallback(async () => {
setLoading(true);
try {
const {html: reponse} = await loadDocs({
api,
orgSlug: organization.slug,
projectSlug,
platform: platform.key as PlatformKey,
});
setHtml(reponse);
window.scrollTo(0, 0);
} catch (err) {
setError(err);
}
setLoading(false);
}, [api, organization.slug, projectSlug, platform.key]);
useEffect(() => {
fetchDocs();
}, [fetchDocs]);
return (
{tct(
`
This is a quick getting started guide. For in-depth instructions
on integrating Sentry with [platform], view
[docLink:our complete documentation].`,
{
platform: platform.name,
docLink: ,
}
)}
{loading ? (
) : error ? (
) : (
)}
);
}
export function ProjectInstallPlatform({location, params, route, router}: Props) {
const organization = useOrganization();
const api = useApi();
const gettingStartedWithProjectContext = useContext(GettingStartedWithProjectContext);
const isSelfHosted = ConfigStore.get('isSelfHosted');
const {projects, initiallyLoaded} = useProjects({
slugs: [params.projectId],
orgId: organization.slug,
});
const loadingProjects = !initiallyLoaded;
const project = !loadingProjects
? projects.find(proj => proj.slug === params.projectId)
: undefined;
const currentPlatformKey = project?.platform ?? 'other';
const currentPlatform = platforms.find(p => p.id === currentPlatformKey);
const [showLoaderOnboarding, setShowLoaderOnboarding] = useState(
currentPlatform?.id === 'javascript'
);
const products = useMemo(
() => (location.query.product ?? []) as ProductSolution[],
[location.query.product]
);
const {
data: projectAlertRules,
isLoading: projectAlertRulesIsLoading,
isError: projectAlertRulesIsError,
} = useApiQuery(
[`/projects/${organization.slug}/${project?.slug}/rules/`],
{
enabled: !!project?.slug,
staleTime: 0,
}
);
useEffect(() => {
setShowLoaderOnboarding(currentPlatform?.id === 'javascript');
}, [currentPlatform?.id]);
useEffect(() => {
if (!project || projectAlertRulesIsLoading || projectAlertRulesIsError) {
return;
}
if (gettingStartedWithProjectContext.project?.id === project.id) {
return;
}
const platformKey = Object.keys(platforms).find(
key => platforms[key].id === project.platform
);
if (!platformKey) {
return;
}
gettingStartedWithProjectContext.setProject({
id: project.id,
name: project.name,
// sometimes the team slug here can be undefined
teamSlug: project.team?.slug,
alertRules: projectAlertRules,
platform: {
...omit(platforms[platformKey], 'id'),
key: platforms[platformKey].id,
} as OnboardingSelectedSDK,
});
}, [
gettingStartedWithProjectContext,
project,
projectAlertRules,
projectAlertRulesIsLoading,
projectAlertRulesIsError,
]);
const heartbeatFooter = !!organization?.features.includes(
'onboarding-heartbeat-footer'
);
const projectDeletionOnBackClick = !!organization?.features.includes(
'onboarding-project-deletion-on-back-click'
);
// This is a feature flag that is currently only enabled for a subset of internal users until the feature is fully implemented,
// but the purpose of the feature is to make the product selection feature in documents available to all users
// and guide them to upgrade to a plan if one of the products is not available on their current plan.
const gettingStartedDocWithProductSelection = !!organization?.features.includes(
'getting-started-doc-with-product-selection'
);
const recentCreatedProject = useRecentCreatedProject({
orgSlug: organization.slug,
projectSlug: project?.slug,
});
const shallProjectBeDeleted =
projectDeletionOnBackClick &&
recentCreatedProject &&
// if the project has received a first error, we don't delete it
recentCreatedProject.firstError === false &&
// if the project has received a first transaction, we don't delete it
recentCreatedProject.firstTransaction === false &&
// if the project has replays, we don't delete it
recentCreatedProject.hasReplays === false &&
// if the project has sessions, we don't delete it
recentCreatedProject.hasSessions === false &&
// if the project is older than one hour, we don't delete it
recentCreatedProject.olderThanOneHour === false;
const platformIntegration = platforms.find(p => p.id === currentPlatformKey);
const platform: Platform = {
key: currentPlatformKey as PlatformKey,
id: platformIntegration?.id,
name: platformIntegration?.name,
link: platformIntegration?.link,
};
const redirectToNeutralDocs = useCallback(() => {
if (!project?.slug) {
return;
}
router.push(
normalizeUrl(
`/organizations/${organization.slug}/projects/${project.slug}/getting-started/`
)
);
}, [organization.slug, project?.slug, router]);
const handleGoBack = useCallback(async () => {
if (!recentCreatedProject) {
return;
}
trackAnalytics('project_creation.back_button_clicked', {
organization,
});
if (shallProjectBeDeleted) {
trackAnalytics('project_creation.data_removal_modal_confirm_button_clicked', {
organization,
platform: recentCreatedProject.slug,
project_id: recentCreatedProject.id,
});
try {
await removeProject({
api,
orgSlug: organization.slug,
projectSlug: recentCreatedProject.slug,
origin: 'getting_started',
});
trackAnalytics('project_creation.data_removed', {
organization,
date_created: recentCreatedProject.dateCreated,
platform: recentCreatedProject.slug,
project_id: recentCreatedProject.id,
});
} catch (error) {
handleXhrErrorResponse('Unable to delete project in project creation', error);
// we don't give the user any feedback regarding this error as this shall be silent
}
}
router.replace(
normalizeUrl(
`/organizations/${organization.slug}/projects/new/?referrer=getting-started&project=${recentCreatedProject.id}`
)
);
}, [api, recentCreatedProject, organization, shallProjectBeDeleted, router]);
const hideLoaderOnboarding = useCallback(() => {
setShowLoaderOnboarding(false);
if (!project?.id || !currentPlatform) {
return;
}
trackAnalytics('onboarding.js_loader_npm_docs_shown', {
organization,
platform: currentPlatform.id,
project_id: project?.id,
});
}, [organization, currentPlatform, project?.id]);
useEffect(() => {
// redirect if platform is not known.
if (!platform.key || platform.key === 'other') {
redirectToNeutralDocs();
}
}, [platform.key, redirectToNeutralDocs]);
if (!project) {
return null;
}
if (!platform.id) {
return ;
}
const issueStreamLink = `/organizations/${organization.slug}/issues/`;
const performanceOverviewLink = `/organizations/${organization.slug}/performance/`;
const showPerformancePrompt = performancePlatforms.includes(platform.id as PlatformKey);
const isGettingStarted = window.location.href.indexOf('getting-started') > 0;
const showDocsWithProductSelection =
gettingStartedDocWithProductSelection &&
(platform.key === 'javascript' || !!platform.key.match('^javascript-([A-Za-z]+)$'));
return (
{!isSelfHosted && showDocsWithProductSelection && (
)}
{t('Configure %(platform)s SDK', {platform: platform.name})}
{
if (!recentCreatedProject) {
return;
}
trackAnalytics('project_creation.data_removal_modal_dismissed', {
organization,
platform: recentCreatedProject.slug,
project_id: recentCreatedProject.id,
});
}}
onRender={() => {
if (!recentCreatedProject) {
return;
}
trackAnalytics('project_creation.data_removal_modal_rendered', {
organization,
platform: recentCreatedProject.slug,
project_id: recentCreatedProject.id,
});
}}
>
} size="sm">
{t('Back to Platform Selection')}
{currentPlatform && showLoaderOnboarding ? (
) : currentPlatform && migratedDocs.includes(currentPlatformKey) ? (
) : (
)}
{isGettingStarted && showPerformancePrompt && (
{({hasFeature}) => {
if (hasFeature) {
return null;
}
return (
{t(
`Your selected platform supports performance, but your organization does not have performance enabled.`
)}
);
}}
)}
{isGettingStarted && heartbeatFooter ? (
) : (
)}
);
}
const StyledButtonBar = styled(ButtonBar)`
margin-top: ${space(3)};
width: max-content;
@media (max-width: ${p => p.theme.breakpoints.small}) {
width: auto;
grid-row-gap: ${space(1)};
grid-auto-flow: row;
}
`;
const StyledPageHeader = styled('div')`
display: flex;
justify-content: space-between;
margin-bottom: ${space(3)};
h2 {
margin: 0;
}
@media (max-width: ${p => p.theme.breakpoints.small}) {
flex-direction: column;
align-items: flex-start;
h2 {
margin-bottom: ${space(2)};
}
}
`;
const StyledAlert = styled(Alert)`
margin-top: ${space(2)};
`;