setupDocs.tsx 12 KB

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