setupDocs.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {motion} from 'framer-motion';
  5. import {Location} from 'history';
  6. import {loadDocs} from 'sentry/actionCreators/projects';
  7. import {Alert} from 'sentry/components/alert';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper';
  11. import {Footer} from 'sentry/components/onboarding/footer';
  12. import {FooterWithViewSampleErrorButton} from 'sentry/components/onboarding/footerWithViewSampleErrorButton';
  13. import {
  14. migratedDocs,
  15. SdkDocumentation,
  16. } from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
  17. import {MissingExampleWarning} from 'sentry/components/onboarding/missingExampleWarning';
  18. import {
  19. ProductSelection,
  20. ProductSolution,
  21. } from 'sentry/components/onboarding/productSelection';
  22. import {PlatformKey} from 'sentry/data/platformCategories';
  23. import platforms from 'sentry/data/platforms';
  24. import {t} from 'sentry/locale';
  25. import {space} from 'sentry/styles/space';
  26. import {Organization, Project} from 'sentry/types';
  27. import {OnboardingPlatformDoc} from 'sentry/types/onboarding';
  28. import {trackAnalytics} from 'sentry/utils/analytics';
  29. import getDynamicText from 'sentry/utils/getDynamicText';
  30. import {platformToIntegrationMap} from 'sentry/utils/integrationUtil';
  31. import {useApiQuery} from 'sentry/utils/queryClient';
  32. import useApi from 'sentry/utils/useApi';
  33. import {useExperiment} from 'sentry/utils/useExperiment';
  34. import useOrganization from 'sentry/utils/useOrganization';
  35. import SetupIntroduction from 'sentry/views/onboarding/components/setupIntroduction';
  36. import {SetupDocsLoader} from 'sentry/views/onboarding/setupDocsLoader';
  37. import FirstEventFooter from './components/firstEventFooter';
  38. import IntegrationSetup from './integrationSetup';
  39. import {StepProps} from './types';
  40. export function DocWithProductSelection({
  41. organization,
  42. location,
  43. projectSlug,
  44. newOrg,
  45. currentPlatform: currentPlatformKey,
  46. }: {
  47. currentPlatform: PlatformKey;
  48. location: Location;
  49. organization: Organization;
  50. projectSlug: Project['slug'];
  51. newOrg?: boolean;
  52. }) {
  53. const products = useMemo<ProductSolution[]>(
  54. () => (location.query.product ?? []) as ProductSolution[],
  55. [location.query.product]
  56. );
  57. const currentPlatform = platforms.find(p => p.id === currentPlatformKey);
  58. const platformName = currentPlatform?.name ?? '';
  59. const loadLocalSdkDocumentation =
  60. currentPlatform && migratedDocs.includes(currentPlatformKey);
  61. const loadPlatform = useMemo(() => {
  62. return products.includes(ProductSolution.PERFORMANCE_MONITORING) &&
  63. products.includes(ProductSolution.SESSION_REPLAY)
  64. ? `${currentPlatformKey}-with-error-monitoring-performance-and-replay`
  65. : products.includes(ProductSolution.PERFORMANCE_MONITORING)
  66. ? `${currentPlatformKey}-with-error-monitoring-and-performance`
  67. : products.includes(ProductSolution.SESSION_REPLAY)
  68. ? `${currentPlatformKey}-with-error-monitoring-and-replay`
  69. : `${currentPlatformKey}-with-error-monitoring`;
  70. }, [products, currentPlatformKey]);
  71. const {data, isLoading, isError, refetch} = useApiQuery<OnboardingPlatformDoc>(
  72. [`/projects/${organization.slug}/${projectSlug}/docs/${loadPlatform}/`],
  73. {
  74. staleTime: Infinity,
  75. enabled:
  76. !!projectSlug &&
  77. !!organization.slug &&
  78. !!loadPlatform &&
  79. !loadLocalSdkDocumentation,
  80. }
  81. );
  82. return (
  83. <Fragment>
  84. {newOrg && (
  85. <SetupIntroduction
  86. stepHeaderText={t('Configure %s SDK', platformName)}
  87. platform={currentPlatformKey}
  88. />
  89. )}
  90. {loadLocalSdkDocumentation ? (
  91. <SdkDocumentation
  92. platform={currentPlatform}
  93. orgSlug={organization.slug}
  94. projectSlug={projectSlug}
  95. activeProductSelection={products}
  96. newOrg={newOrg}
  97. />
  98. ) : (
  99. <Fragment>
  100. <ProductSelection
  101. defaultSelectedProducts={[
  102. ProductSolution.PERFORMANCE_MONITORING,
  103. ProductSolution.SESSION_REPLAY,
  104. ]}
  105. />
  106. {isLoading ? (
  107. <LoadingIndicator />
  108. ) : isError ? (
  109. <LoadingError
  110. message={t(
  111. 'Failed to load documentation for the %s platform.',
  112. platformName
  113. )}
  114. onRetry={refetch}
  115. />
  116. ) : (
  117. getDynamicText({
  118. value: (
  119. <DocsWrapper>
  120. <DocumentationWrapper
  121. dangerouslySetInnerHTML={{__html: data?.html ?? ''}}
  122. />
  123. <MissingExampleWarning
  124. platform={currentPlatformKey}
  125. platformDocs={{
  126. html: data?.html ?? '',
  127. link: data?.link ?? '',
  128. }}
  129. />
  130. </DocsWrapper>
  131. ),
  132. fixed: (
  133. <Alert type="warning">
  134. Platform documentation is not rendered in for tests in CI
  135. </Alert>
  136. ),
  137. })
  138. )}
  139. </Fragment>
  140. )}
  141. </Fragment>
  142. );
  143. }
  144. function ProjectDocs(props: {
  145. hasError: boolean;
  146. onRetry: () => void;
  147. organization: Organization;
  148. platform: PlatformKey | null;
  149. platformDocs: OnboardingPlatformDoc | null;
  150. project: Project;
  151. }) {
  152. const currentPlatform = props.platform ?? props.project?.platform ?? 'other';
  153. return (
  154. <Fragment>
  155. <SetupIntroduction
  156. stepHeaderText={t(
  157. 'Configure %s SDK',
  158. platforms.find(p => p.id === currentPlatform)?.name ?? ''
  159. )}
  160. platform={currentPlatform}
  161. />
  162. {getDynamicText({
  163. value: !props.hasError ? (
  164. props.platformDocs !== null && (
  165. <DocsWrapper key={props.platformDocs.html}>
  166. <DocumentationWrapper
  167. dangerouslySetInnerHTML={{__html: props.platformDocs.html}}
  168. />
  169. <MissingExampleWarning
  170. platform={props.platform}
  171. platformDocs={props.platformDocs}
  172. />
  173. </DocsWrapper>
  174. )
  175. ) : (
  176. <LoadingError
  177. message={t(
  178. 'Failed to load documentation for the %s platform.',
  179. props.project?.platform
  180. )}
  181. onRetry={props.onRetry}
  182. />
  183. ),
  184. fixed: (
  185. <Alert type="warning">
  186. Platform documentation is not rendered in for tests in CI
  187. </Alert>
  188. ),
  189. })}
  190. </Fragment>
  191. );
  192. }
  193. function SetupDocs({route, router, location, recentCreatedProject: project}: StepProps) {
  194. const api = useApi();
  195. const organization = useOrganization();
  196. const {
  197. logExperiment: newFooterLogExperiment,
  198. experimentAssignment: newFooterAssignment,
  199. } = useExperiment('OnboardingNewFooterExperiment', {
  200. logExperimentOnMount: false,
  201. });
  202. const heartbeatFooter = !!organization?.features.includes(
  203. 'onboarding-heartbeat-footer'
  204. );
  205. const products = useMemo<ProductSolution[]>(
  206. () => (location.query.product ?? []) as ProductSolution[],
  207. [location.query.product]
  208. );
  209. // SDK instrumentation
  210. const [hasError, setHasError] = useState(false);
  211. const [platformDocs, setPlatformDocs] = useState<OnboardingPlatformDoc | null>(null);
  212. const [loadedPlatform, setLoadedPlatform] = useState<PlatformKey | null>(null);
  213. const currentPlatformKey = loadedPlatform ?? project?.platform ?? 'other';
  214. const [showLoaderOnboarding, setShowLoaderOnboarding] = useState(
  215. currentPlatformKey === 'javascript'
  216. );
  217. useEffect(() => {
  218. setShowLoaderOnboarding(currentPlatformKey === 'javascript');
  219. }, [currentPlatformKey]);
  220. const integrationSlug = project?.platform && platformToIntegrationMap[project.platform];
  221. const [integrationUseManualSetup, setIntegrationUseManualSetup] = useState(false);
  222. const showIntegrationOnboarding = integrationSlug && !integrationUseManualSetup;
  223. const showDocsWithProductSelection =
  224. currentPlatformKey.match('^javascript-([A-Za-z]+)$') ??
  225. (showLoaderOnboarding === false && currentPlatformKey === 'javascript');
  226. const hideLoaderOnboarding = useCallback(() => {
  227. setShowLoaderOnboarding(false);
  228. if (!project?.id) {
  229. return;
  230. }
  231. trackAnalytics('onboarding.js_loader_npm_docs_shown', {
  232. organization,
  233. platform: currentPlatformKey,
  234. project_id: project?.id,
  235. });
  236. }, [organization, currentPlatformKey, project?.id]);
  237. const fetchData = useCallback(async () => {
  238. // TODO: add better error handling logic
  239. if (!project?.platform) {
  240. return;
  241. }
  242. // this will be fetched in the DocWithProductSelection component
  243. if (showDocsWithProductSelection) {
  244. return;
  245. }
  246. // Show loader setup for base javascript platform
  247. if (showLoaderOnboarding) {
  248. return;
  249. }
  250. if (showIntegrationOnboarding) {
  251. setLoadedPlatform(project.platform);
  252. setPlatformDocs(null);
  253. setHasError(false);
  254. return;
  255. }
  256. try {
  257. const loadedDocs = await loadDocs({
  258. api,
  259. orgSlug: organization.slug,
  260. projectSlug: project.slug,
  261. platform: project.platform as PlatformKey,
  262. });
  263. setPlatformDocs(loadedDocs);
  264. setLoadedPlatform(project.platform);
  265. setHasError(false);
  266. } catch (error) {
  267. setHasError(error);
  268. throw error;
  269. }
  270. }, [
  271. project?.slug,
  272. project?.platform,
  273. api,
  274. organization.slug,
  275. showDocsWithProductSelection,
  276. showIntegrationOnboarding,
  277. showLoaderOnboarding,
  278. ]);
  279. useEffect(() => {
  280. fetchData();
  281. }, [fetchData, location.query.product, project?.platform]);
  282. // log experiment on mount if feature enabled
  283. useEffect(() => {
  284. if (heartbeatFooter) {
  285. newFooterLogExperiment();
  286. }
  287. }, [newFooterLogExperiment, heartbeatFooter]);
  288. if (!project) {
  289. return null;
  290. }
  291. const currentPlatform = platforms.find(p => p.id === currentPlatformKey);
  292. const platformName = currentPlatform?.name ?? '';
  293. const loadLocalSdkDocumentation =
  294. currentPlatform && migratedDocs.includes(currentPlatformKey);
  295. return (
  296. <Fragment>
  297. <Wrapper>
  298. <MainContent>
  299. {showIntegrationOnboarding ? (
  300. <IntegrationSetup
  301. integrationSlug={integrationSlug}
  302. project={project}
  303. onClickManualSetup={() => {
  304. setIntegrationUseManualSetup(true);
  305. }}
  306. />
  307. ) : showLoaderOnboarding ? (
  308. <Fragment>
  309. <SetupIntroduction
  310. stepHeaderText={t(
  311. 'Configure %s SDK',
  312. platforms.find(p => p.id === currentPlatformKey)?.name ?? ''
  313. )}
  314. platform={currentPlatformKey}
  315. />
  316. <SetupDocsLoader
  317. organization={organization}
  318. project={project}
  319. location={location}
  320. platform={loadedPlatform}
  321. close={hideLoaderOnboarding}
  322. />
  323. </Fragment>
  324. ) : loadLocalSdkDocumentation ? (
  325. <Fragment>
  326. <SetupIntroduction
  327. stepHeaderText={t('Configure %s SDK', platformName)}
  328. platform={currentPlatformKey}
  329. />
  330. <SdkDocumentation
  331. platform={currentPlatform}
  332. orgSlug={organization.slug}
  333. projectSlug={project.slug}
  334. activeProductSelection={products}
  335. newOrg
  336. />
  337. </Fragment>
  338. ) : (
  339. <ProjectDocs
  340. platform={loadedPlatform}
  341. project={project}
  342. hasError={hasError}
  343. platformDocs={platformDocs}
  344. onRetry={fetchData}
  345. organization={organization}
  346. />
  347. )}
  348. </MainContent>
  349. </Wrapper>
  350. {heartbeatFooter ? (
  351. newFooterAssignment === 'variant2' ? (
  352. <FooterWithViewSampleErrorButton
  353. projectSlug={project.slug}
  354. projectId={project.id}
  355. route={route}
  356. router={router}
  357. location={location}
  358. newOrg
  359. />
  360. ) : newFooterAssignment === 'variant1' ? (
  361. <Footer
  362. projectSlug={project.slug}
  363. projectId={project.id}
  364. route={route}
  365. router={router}
  366. location={location}
  367. newOrg
  368. />
  369. ) : (
  370. <FirstEventFooter
  371. project={project}
  372. organization={organization}
  373. isLast
  374. onClickSetupLater={() => {
  375. const orgIssuesURL = `/organizations/${organization.slug}/issues/?project=${project.id}&referrer=onboarding-setup-docs`;
  376. trackAnalytics('growth.onboarding_clicked_setup_platform_later', {
  377. organization,
  378. platform: currentPlatformKey,
  379. project_id: project.id,
  380. });
  381. browserHistory.push(orgIssuesURL);
  382. }}
  383. />
  384. )
  385. ) : (
  386. <FirstEventFooter
  387. project={project}
  388. organization={organization}
  389. isLast
  390. onClickSetupLater={() => {
  391. const orgIssuesURL = `/organizations/${organization.slug}/issues/?project=${project.id}&referrer=onboarding-setup-docs`;
  392. trackAnalytics('growth.onboarding_clicked_setup_platform_later', {
  393. organization,
  394. platform: currentPlatformKey,
  395. project_id: project.id,
  396. });
  397. browserHistory.push(orgIssuesURL);
  398. }}
  399. />
  400. )}
  401. </Fragment>
  402. );
  403. }
  404. export default SetupDocs;
  405. const AnimatedContentWrapper = styled(motion.div)`
  406. overflow: hidden;
  407. `;
  408. AnimatedContentWrapper.defaultProps = {
  409. initial: {
  410. height: 0,
  411. },
  412. animate: {
  413. height: 'auto',
  414. },
  415. exit: {
  416. height: 0,
  417. },
  418. };
  419. const DocsWrapper = styled(motion.div)``;
  420. DocsWrapper.defaultProps = {
  421. initial: {opacity: 0, y: 40},
  422. animate: {opacity: 1, y: 0},
  423. exit: {opacity: 0},
  424. };
  425. const Wrapper = styled('div')`
  426. display: flex;
  427. flex-direction: row;
  428. margin: ${space(2)};
  429. justify-content: center;
  430. `;
  431. const MainContent = styled('div')`
  432. max-width: 850px;
  433. min-width: 0;
  434. flex-grow: 1;
  435. `;