setupDocs.tsx 14 KB

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