setupDocs.tsx 11 KB

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