setupDocs.tsx 13 KB

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