import {Fragment, useCallback, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import {motion} from 'framer-motion'; import {Location} from 'history'; import beautify from 'js-beautify'; import {Button} from 'sentry/components/button'; import {CodeSnippet} from 'sentry/components/codeSnippet'; import HookOrDefault from 'sentry/components/hookOrDefault'; import ExternalLink from 'sentry/components/links/externalLink'; import LoadingError from 'sentry/components/loadingError'; import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper'; import { ProductSelection, ProductSolution, } from 'sentry/components/onboarding/productSelection'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {PlatformKey} from 'sentry/types'; import {Organization, Project, ProjectKey} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse'; import {decodeList} from 'sentry/utils/queryString'; import useApi from 'sentry/utils/useApi'; const ProductSelectionAvailabilityHook = HookOrDefault({ hookName: 'component:product-selection-availability', defaultComponent: ProductSelection, }); export function SetupDocsLoader({ organization, location, project, platform, close, }: { close: () => void; location: Location; organization: Organization; platform: PlatformKey | null; project: Project; }) { const api = useApi(); const currentPlatform = platform ?? project?.platform ?? 'other'; const [projectKey, setProjectKey] = useState(null); const [hasLoadingError, setHasLoadingError] = useState(false); const [projectKeyUpdateError, setProjectKeyUpdateError] = useState(false); const productsQuery = (location.query.product as ProductSolution | ProductSolution[] | undefined) ?? []; const products = decodeList(productsQuery) as ProductSolution[]; const fetchData = useCallback(async () => { const keysApiUrl = `/projects/${organization.slug}/${project.slug}/keys/`; try { const loadedKeys = await api.requestPromise(keysApiUrl); if (loadedKeys.length === 0) { setHasLoadingError(true); return; } setProjectKey(loadedKeys[0]); setHasLoadingError(false); } catch (error) { setHasLoadingError(error); throw error; } }, [api, organization.slug, project.slug]); // Automatically update the products on the project key when the user changes the product selection // Note that on initial visit, this will also update the project key with the default products (=all products) // This DOES NOT take into account any initial products that may already be set on the project key - they will always be overwritten! const handleUpdateSelectedProducts = useCallback(async () => { const keyId = projectKey?.id; if (!keyId) { return; } const newDynamicSdkLoaderOptions: ProjectKey['dynamicSdkLoaderOptions'] = { hasPerformance: false, hasReplay: false, hasDebug: false, }; products.forEach(product => { // eslint-disable-next-line default-case switch (product) { case ProductSolution.PERFORMANCE_MONITORING: newDynamicSdkLoaderOptions.hasPerformance = true; break; case ProductSolution.SESSION_REPLAY: newDynamicSdkLoaderOptions.hasReplay = true; break; } }); try { await api.requestPromise( `/projects/${organization.slug}/${project.slug}/keys/${keyId}/`, { method: 'PUT', data: { dynamicSdkLoaderOptions: newDynamicSdkLoaderOptions, }, } ); setProjectKeyUpdateError(false); } catch (error) { const message = t('Unable to updated dynamic SDK loader configuration'); handleXhrErrorResponse(message, error); setProjectKeyUpdateError(true); } }, [api, organization.slug, project.slug, projectKey?.id, products]); const track = useCallback(() => { if (!project?.id) { return; } trackAnalytics('onboarding.setup_loader_docs_rendered', { organization, platform: currentPlatform, project_id: project?.id, }); }, [organization, currentPlatform, project?.id]); useEffect(() => { fetchData(); }, [fetchData, organization.slug, project.slug]); useEffect(() => { handleUpdateSelectedProducts(); }, [handleUpdateSelectedProducts, location.query.product]); useEffect(() => { track(); }, [track]); return ( {projectKeyUpdateError && ( )} {!hasLoadingError ? ( projectKey !== null && ( ) ) : ( )} ); } function ProjectKeyInfo({ projectKey, platform, organization, project, products, }: { organization: Organization; platform: PlatformKey | null; products: ProductSolution[]; project: Project; projectKey: ProjectKey; }) { const [showOptionalConfig, setShowOptionalConfig] = useState(false); const loaderLink = projectKey.dsn.cdn; const currentPlatform = platform ?? project?.platform ?? 'other'; const hasPerformance = products.includes(ProductSolution.PERFORMANCE_MONITORING); const hasSessionReplay = products.includes(ProductSolution.SESSION_REPLAY); const configCodeSnippet = beautify.html( ``, {indent_size: 2} ); const verifyCodeSnippet = beautify.html( ``, {indent_size: 2} ); const toggleOptionalConfiguration = useCallback(() => { const show = !showOptionalConfig; setShowOptionalConfig(show); if (show) { trackAnalytics('onboarding.js_loader_optional_configuration_shown', { organization, platform: currentPlatform, project_id: project.id, }); } }, [organization, project.id, currentPlatform, showOptionalConfig]); return (

{t('Install')}

{t('Add this script tag to the top of the page:')}

{beautify.html( ``, {indent_size: 2, wrap_attributes: 'force-expand-multiline'} )} } aria-label={t('Toggle optional configuration')} onClick={toggleOptionalConfiguration} />

{t('Configuration (Optional)')}

{showOptionalConfig && (

{t( "Initialise Sentry as early as possible in your application's lifecycle." )}

{configCodeSnippet}
)}

{t('Verify')}

{t( "This snippet contains an intentional error and can be used as a test to make sure that everything's working as expected." )}

{verifyCodeSnippet}

{t('Next Steps')}

  • {t('Source Maps')} {': '} {t('Learn how to enable readable stack traces in your Sentry errors.')}
  • {t('SDK Configuration')} {': '} {t('Learn how to configure your SDK using our Loader Script')}
  • {!products.includes(ProductSolution.PERFORMANCE_MONITORING) && (
  • {t('Performance Monitoring')} {': '} {t( 'Track down transactions to connect the dots between 10-second page loads and poor-performing API calls or slow database queries.' )}
  • )} {!products.includes(ProductSolution.SESSION_REPLAY) && (
  • {t('Session Replay')} {': '} {t( 'Get to the root cause of an error or latency issue faster by seeing all the technical details related to that issue in one visual replay on your web application.' )}
  • )}
); } const DocsWrapper = styled(motion.div)``; const OptionalConfigWrapper = styled('div')` display: flex; cursor: pointer; `; const ToggleButton = styled(Button)` &, :hover { color: ${p => p.theme.gray500}; } `; const Divider = styled('hr')<{withBottomMargin?: boolean}>` height: 1px; width: 100%; background: ${p => p.theme.border}; border: none; `;