import {Fragment, useCallback, useEffect, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import {motion} from 'framer-motion'; import * as qs from 'query-string'; import {loadDocs} from 'sentry/actionCreators/projects'; import {Alert} from 'sentry/components/alert'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingError from 'sentry/components/loadingError'; import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper'; import {Footer} from 'sentry/components/onboarding/footer'; import {FooterWithViewSampleErrorButton} from 'sentry/components/onboarding/footerWithViewSampleErrorButton'; import {PRODUCT, ProductSelection} from 'sentry/components/onboarding/productSelection'; import {PlatformKey} from 'sentry/data/platformCategories'; import platforms, {ReactDocVariant} from 'sentry/data/platforms'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {Organization, Project} from 'sentry/types'; import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent'; import getDynamicText from 'sentry/utils/getDynamicText'; import {platformToIntegrationMap} from 'sentry/utils/integrationUtil'; import useApi from 'sentry/utils/useApi'; import {useExperiment} from 'sentry/utils/useExperiment'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; import SetupIntroduction from 'sentry/views/onboarding/components/setupIntroduction'; import FirstEventFooter from './components/firstEventFooter'; import ProjectSidebarSection from './components/projectSidebarSection'; import IntegrationSetup from './integrationSetup'; import {StepProps} from './types'; import {usePersistedOnboardingState} from './utils'; /** * The documentation will include the following string should it be missing the * verification example, which currently a lot of docs are. */ const INCOMPLETE_DOC_FLAG = 'TODO-ADD-VERIFICATION-EXAMPLE'; type PlatformDoc = {html: string; link: string}; type Props = { search: string; } & StepProps; function ProjectDocs(props: { hasError: boolean; onRetry: () => void; organization: Organization; platform: PlatformKey | null; platformDocs: PlatformDoc | null; project: Project; }) { const testOnlyAlert = ( Platform documentation is not rendered in for tests in CI ); const missingExampleWarning = () => { const missingExample = props.platformDocs && props.platformDocs.html.includes(INCOMPLETE_DOC_FLAG); if (!missingExample) { return null; } return ( {tct( `Looks like this getting started example is still undergoing some work and doesn't include an example for triggering an event quite yet. If you have trouble sending your first event be sure to consult the [docsLink:full documentation] for [platform].`, { docsLink: , platform: platforms.find(p => p.id === props.platform)?.name, } )} ); }; const docs = props.platformDocs !== null && ( {missingExampleWarning()} ); const loadingError = ( ); const currentPlatform = props.platform ?? props.project?.platform ?? 'other'; return ( p.id === currentPlatform)?.name ?? '' )} platform={currentPlatform} /> {currentPlatform === 'javascript-react' && props.organization.features?.includes( 'onboarding-docs-with-product-selection' ) && } {getDynamicText({ value: !props.hasError ? docs : loadingError, fixed: testOnlyAlert, })} ); } function SetupDocs({search, route, router, location, ...props}: Props) { const api = useApi(); const organization = useOrganization(); const {projects: rawProjects} = useProjects(); const [clientState, setClientState] = usePersistedOnboardingState(); const [selectedProjectSlug, _setSelectedProjectSlug] = useState( props.selectedProjectSlug ); const {logExperiment, experimentAssignment} = useExperiment( 'OnboardingNewFooterExperiment', { logExperimentOnMount: false, } ); const singleSelectPlatform = !!organization?.features.includes( 'onboarding-remove-multiselect-platform' ); const heartbeatFooter = !!organization?.features.includes( 'onboarding-heartbeat-footer' ); const selectedPlatforms = clientState?.selectedPlatforms || []; const platformToProjectIdMap = clientState?.platformToProjectIdMap || {}; // id is really slug here const projectSlugs = selectedPlatforms .map(platform => platformToProjectIdMap[platform]) .filter((slug): slug is string => slug !== undefined); const selectedProjectsSet = new Set(projectSlugs); // get projects in the order they appear in selectedPlatforms const projects = projectSlugs .map(slug => rawProjects.find(project => project.slug === slug)) .filter((project): project is Project => project !== undefined); // SDK instrumentation const [hasError, setHasError] = useState(false); const [platformDocs, setPlatformDocs] = useState(null); const [loadedPlatform, setLoadedPlatform] = useState(null); // store what projects have sent first event in state based project.firstEvent const [hasFirstEventMap, setHasFirstEventMap] = useState>( projects.reduce((accum, project: Project) => { accum[project.id] = !!project.firstEvent; return accum; }, {} as Record) ); const checkProjectHasFirstEvent = (project: Project) => { return !!hasFirstEventMap[project.id]; }; const {project_id: rawProjectId} = qs.parse(search); const rawProjectIndex = projects.findIndex(p => p.id === rawProjectId); const firstProjectNoError = projects.findIndex(p => selectedProjectsSet.has(p.slug)); // Select a project based on search params. If non exist, use the first project without first event. const projectIndex = rawProjectIndex >= 0 ? rawProjectIndex : firstProjectNoError; const project = projects[projectIndex] ?? rawProjects.find(p => p.slug === selectedProjectSlug); // find the next project that doesn't have a first event const nextProject = projects.find( (p, i) => i > projectIndex && !checkProjectHasFirstEvent(p) ); const integrationSlug = project?.platform && platformToIntegrationMap[project.platform]; const [integrationUseManualSetup, setIntegrationUseManualSetup] = useState(false); const currentPlatform = loadedPlatform ?? project?.platform ?? 'other'; const fetchData = useCallback(async () => { // TODO: add better error handling logic if (!project?.platform) { return; } if (integrationSlug && !integrationUseManualSetup) { setLoadedPlatform(project.platform); setPlatformDocs(null); setHasError(false); return; } let loadPlatform = String(project.platform); if ( organization.features?.includes('onboarding-docs-with-product-selection') && project.platform === 'javascript-react' ) { // This is an experiment we are doing with react. // In this experiment we let the user choose which Sentry product he would like to have in his `Sentry.Init()` // and the docs will reflect that. const products = location.query.product ?? []; if ( products.includes(PRODUCT.PERFORMANCE_MONITORING) && products.includes(PRODUCT.SESSION_REPLAY) ) { loadPlatform = ReactDocVariant.ErrorMonitoringPerformanceAndReplay; } else if (products.includes(PRODUCT.PERFORMANCE_MONITORING)) { loadPlatform = ReactDocVariant.ErrorMonitoringAndPerformance; } else if (products.includes(PRODUCT.SESSION_REPLAY)) { loadPlatform = ReactDocVariant.ErrorMonitoringAndSessionReplay; } else { loadPlatform = ReactDocVariant.ErrorMonitoring; } } try { const loadedDocs = await loadDocs({ api, orgSlug: organization.slug, projectSlug: project.slug, platform: loadPlatform as PlatformKey, }); setPlatformDocs(loadedDocs); setLoadedPlatform(project.platform); setHasError(false); } catch (error) { setHasError(error); throw error; } }, [ project?.slug, project?.platform, api, organization.slug, organization.features, integrationSlug, integrationUseManualSetup, location.query.product, ]); useEffect(() => { fetchData(); }, [fetchData, location.query.product, project?.platform]); // log experiment on mount if feature enabled useEffect(() => { if (heartbeatFooter) { logExperiment(); } }, [logExperiment, heartbeatFooter]); if (!project) { return null; } const setNewProject = (newProjectId: string) => { setLoadedPlatform(null); setPlatformDocs(null); setHasError(false); setIntegrationUseManualSetup(false); const searchParams = new URLSearchParams({ sub_step: 'project', project_id: newProjectId, }); browserHistory.push(`${window.location.pathname}?${searchParams}`); clientState && setClientState({ ...clientState, state: 'projects_selected', url: `setup-docs/?${searchParams}`, }); }; const selectProject = (newProjectId: string) => { const matchedProject = projects.find(p => p.id === newProjectId); trackAdvancedAnalyticsEvent('growth.onboarding_clicked_project_in_sidebar', { organization, platform: matchedProject?.platform || 'unknown', }); setNewProject(newProjectId); }; return ( {!singleSelectPlatform && ( [ platform, platformToProjectIdMap[platform], ]) )} activeProject={project} {...{checkProjectHasFirstEvent, selectProject}} /> )} {integrationSlug && !integrationUseManualSetup ? ( { setIntegrationUseManualSetup(true); }} /> ) : ( )} {heartbeatFooter ? ( experimentAssignment === 'variant2' ? ( ) : experimentAssignment === 'variant1' ? (