profilingOnboardingModal.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  5. import Button, {ButtonPropsWithoutAriaLabel} from 'sentry/components/button';
  6. import {SelectField} from 'sentry/components/forms';
  7. import {SelectFieldProps} from 'sentry/components/forms/selectField';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import List from 'sentry/components/list';
  10. import Tag from 'sentry/components/tag';
  11. import {IconOpen} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import space from 'sentry/styles/space';
  14. import {Project} from 'sentry/types/project';
  15. import useProjects from 'sentry/utils/useProjects';
  16. // This is just a doubly linked list of steps
  17. interface OnboardingStep {
  18. current: React.ComponentType<OnboardingStepProps>;
  19. next: OnboardingStep | null;
  20. previous: OnboardingStep | null;
  21. }
  22. type OnboardingRouterState = [OnboardingStep, (step: OnboardingStep | null) => void];
  23. function useOnboardingRouter(initialStep: OnboardingStep): OnboardingRouterState {
  24. const [state, setState] = useState(initialStep);
  25. const toStep = useCallback((nextStep: OnboardingStep | null) => {
  26. // For ergonomics, else we need to move everything to consts so that typescript can infer non nullable types
  27. if (nextStep === null) {
  28. return;
  29. }
  30. setState(current => {
  31. const next = {...nextStep, next: null, previous: current};
  32. // Add the edges between the old and the new step
  33. current.next = next;
  34. next.previous = current;
  35. // Return the next step
  36. return next;
  37. });
  38. }, []);
  39. return [state, toStep];
  40. }
  41. // The wrapper component for all of the onboarding steps. Keeps track of the current step
  42. // and all state. This ensures that moving from step to step does not require users to redo their actions
  43. // and each step can just re-initialize with the values that the user has already selected.
  44. export function ProfilingOnboardingModal(props: ModalRenderProps) {
  45. const [state, toStep] = useOnboardingRouter({
  46. previous: null,
  47. current: SelectProjectStep,
  48. next: null,
  49. });
  50. const [project, setProject] = useState<Project | null>(null);
  51. return (
  52. <state.current
  53. {...props}
  54. toStep={toStep}
  55. step={state}
  56. project={project}
  57. setProject={setProject}
  58. />
  59. );
  60. }
  61. // Generate an option for the select field from project
  62. function asSelectOption(
  63. project: Project,
  64. options: {disabled: boolean}
  65. ): SelectFieldProps<Project>['options'][0]['options'] {
  66. return {
  67. label: project.name,
  68. value: project,
  69. disabled: options.disabled,
  70. leadingItems: project.platform ? <PlatformIcon platform={project.platform} /> : null,
  71. };
  72. }
  73. const platformToInstructionsMapping: Record<
  74. string,
  75. React.ComponentType<OnboardingStepProps>
  76. > = {
  77. android: AndroidSendDebugFilesInstruction,
  78. 'apple-ios': IOSSendDebugFilesInstruction,
  79. };
  80. // Splits a list of projects into supported and unsuported list
  81. function splitProjectsByProfilingSupport(projects: Project[]): {
  82. supported: Project[];
  83. unsupported: Project[];
  84. } {
  85. const supported: Project[] = [];
  86. const unsupported: Project[] = [];
  87. for (const project of projects) {
  88. if (project.platform && platformToInstructionsMapping[project.platform]) {
  89. supported.push(project);
  90. } else {
  91. unsupported.push(project);
  92. }
  93. }
  94. return {supported, unsupported};
  95. }
  96. // Individual modal steps are defined here.
  97. // We proxy the modal props to each individaul modal component
  98. // so that each can build their own modal and they can remain independent.
  99. interface OnboardingStepProps extends ModalRenderProps {
  100. project: Project | null;
  101. setProject: React.Dispatch<React.SetStateAction<Project | null>>;
  102. step: OnboardingStep;
  103. toStep: OnboardingRouterState[1];
  104. }
  105. function SelectProjectStep({
  106. Body: ModalBody,
  107. Header: ModalHeader,
  108. Footer: ModalFooter,
  109. closeModal,
  110. toStep,
  111. step,
  112. project,
  113. setProject,
  114. }: OnboardingStepProps) {
  115. const {projects} = useProjects();
  116. const onFormSubmit = useCallback(
  117. (evt: React.FormEvent) => {
  118. evt.preventDefault();
  119. if (!project?.platform) {
  120. return;
  121. }
  122. const nextStep = platformToInstructionsMapping[project.platform];
  123. if (nextStep === undefined) {
  124. throw new TypeError(
  125. "Platform doesn't have a onboarding step, user should not be able to select it"
  126. );
  127. }
  128. toStep({
  129. previous: step,
  130. current: nextStep,
  131. next: null,
  132. });
  133. },
  134. [project, step, toStep]
  135. );
  136. const projectSelectOptions = useMemo((): SelectFieldProps<Project>['options'] => {
  137. const {supported: supportedProjects, unsupported: unsupporedProjects} =
  138. splitProjectsByProfilingSupport(projects);
  139. return [
  140. {
  141. label: t('Supported'),
  142. options: supportedProjects.map(p => asSelectOption(p, {disabled: false})),
  143. },
  144. {
  145. label: t('Unsupported'),
  146. options: unsupporedProjects.map(p => asSelectOption(p, {disabled: true})),
  147. },
  148. ];
  149. }, [projects]);
  150. return (
  151. <ModalBody>
  152. <ModalHeader>
  153. <h3>{t('Set Up Profiling')}</h3>
  154. </ModalHeader>
  155. <form onSubmit={onFormSubmit}>
  156. <StyledList symbol="colored-numeric">
  157. <li>
  158. <StepTitle>
  159. <label htmlFor="project-select">{t('Select a project')}</label>
  160. </StepTitle>
  161. <div>
  162. <StyledSelectField
  163. id="project-select"
  164. name="select"
  165. options={projectSelectOptions}
  166. onChange={setProject}
  167. />
  168. </div>
  169. </li>
  170. {project?.platform === 'android' ? <AndroidInstallSteps /> : null}
  171. {project?.platform === 'apple-ios' ? <IOSInstallSteps /> : null}
  172. </StyledList>
  173. <ModalFooter>
  174. <ModalActions>
  175. <DocsLink />
  176. <div>
  177. <StepIndicator>{t('Step 1 of 2')}</StepIndicator>
  178. <PreviousStepButton type="button" onClick={closeModal} />
  179. <NextStepButton
  180. disabled={
  181. !(project?.platform && platformToInstructionsMapping[project.platform])
  182. }
  183. type="submit"
  184. />
  185. </div>
  186. </ModalActions>
  187. </ModalFooter>
  188. </form>
  189. </ModalBody>
  190. );
  191. }
  192. function AndroidInstallSteps() {
  193. return (
  194. <Fragment>
  195. <li>
  196. <StepTitle>{t('Update your projects SDK version')}</StepTitle>
  197. <p>
  198. {t(
  199. 'Make sure your SDKs are upgraded to at least version 6.0.0 (sentry-android).'
  200. )}
  201. </p>
  202. </li>
  203. <li>
  204. <StepTitle>{t('Setup Performance Monitoring')}</StepTitle>
  205. {t(
  206. `For Sentry to ingest profiles, we first require you to setup performance monitoring. To set up performance monitoring,`
  207. )}{' '}
  208. <ExternalLink
  209. openInNewTab
  210. href="https://docs.sentry.io/platforms/android/performance/"
  211. >
  212. {t('follow our step by step instructions here.')}
  213. </ExternalLink>
  214. </li>
  215. <li>
  216. <StepTitle>{t('Set Up Profiling')}</StepTitle>
  217. <CodeContainer>
  218. {`<application>
  219. <meta-data android:name="io.sentry.dsn" android:value="..." />
  220. <meta-data android:name="io.sentry.traces.sample-rate" android:value="1.0" />
  221. <meta-data android:name="io.sentry.traces.profiling.enable" android:value="true" />
  222. </application>`}
  223. </CodeContainer>
  224. </li>
  225. </Fragment>
  226. );
  227. }
  228. function IOSInstallSteps() {
  229. return (
  230. <Fragment>
  231. <li>
  232. <StepTitle>{t('Update your projects SDK version')}</StepTitle>
  233. <p>
  234. {t(
  235. 'Make sure your SDKs are upgraded to at least version 7.23.0 (sentry-cocoa).'
  236. )}
  237. </p>
  238. </li>
  239. <li>
  240. <StepTitle>{t('Setup Performance Monitoring')}</StepTitle>
  241. {t(
  242. `For Sentry to ingest profiles, we first require you to setup performance monitoring. To set up performance monitoring,`
  243. )}{' '}
  244. <ExternalLink
  245. openInNewTab
  246. href="https://docs.sentry.io/platforms/apple/guides/ios/performance/"
  247. >
  248. {t('follow our step by step instructions here.')}
  249. </ExternalLink>
  250. </li>
  251. <li>
  252. <StepTitle>
  253. {t('Enable profiling in your app by configuring the SDKs like below:')}
  254. </StepTitle>
  255. <CodeContainer>{`SentrySDK.start { options in
  256. options.dsn = "..."
  257. options.tracesSampleRate = 1.0 // Make sure transactions are enabled
  258. options.enableProfiling = true
  259. }`}</CodeContainer>
  260. </li>
  261. </Fragment>
  262. );
  263. }
  264. const StyledList = styled(List)`
  265. position: relative;
  266. li {
  267. margin-bottom: ${space(3)};
  268. }
  269. `;
  270. const StyledSelectField = styled(SelectField)`
  271. padding: 0;
  272. border-bottom: 0;
  273. > div {
  274. width: 100%;
  275. padding-left: 0;
  276. }
  277. `;
  278. function AndroidSendDebugFilesInstruction({
  279. Body: ModalBody,
  280. Header: ModalHeader,
  281. Footer: ModalFooter,
  282. closeModal,
  283. toStep,
  284. step,
  285. }: OnboardingStepProps) {
  286. return (
  287. <ModalBody>
  288. <ModalHeader>
  289. <h3>{t('Set Up Profiling')}</h3>
  290. </ModalHeader>
  291. <p>
  292. {t(
  293. `If you want to see de-obfuscated stack traces, you'll need to use ProGuard with Sentry. To do so, upload the ProGuard mapping files by either the recommended method of using our Gradle integration or manually by using sentry-cli.`
  294. )}{' '}
  295. <ExternalLink href="https://docs.sentry.io/product/cli/dif/">
  296. {t('Learn more about Debug Information Files.')}
  297. </ExternalLink>
  298. </p>
  299. <OptionsContainer>
  300. <OptionTitleContainer>
  301. <OptionTitle>{t('Option 1')}</OptionTitle> <Tag>{t('Recommended')}</Tag>
  302. </OptionTitleContainer>
  303. <OptionTitleContainer>
  304. <OptionTitle>{t('Option 2')}</OptionTitle>
  305. </OptionTitleContainer>
  306. </OptionsContainer>
  307. <OptionsContainer>
  308. <Option>
  309. <ExternalOptionTitle href="https://docs.sentry.io/platforms/android/proguard/">
  310. {t('Proguard and DexGuard')}
  311. <IconOpen />
  312. </ExternalOptionTitle>
  313. <p>{t('Upload ProGuard files using our Gradle plugin.')}</p>
  314. </Option>
  315. <Option>
  316. <ExternalOptionTitle href="https://docs.sentry.io/product/cli/dif/#uploading-files">
  317. {t('Sentry-cli')}
  318. <IconOpen />
  319. </ExternalOptionTitle>
  320. <p>{t('Validate and upload debug files using our cli tool.')}</p>
  321. </Option>
  322. </OptionsContainer>
  323. <ModalFooter>
  324. <ModalActions>
  325. <DocsLink />
  326. <div>
  327. <StepIndicator>{t('Step 2 of 2')}</StepIndicator>
  328. {step.previous ? (
  329. <PreviousStepButton onClick={() => toStep(step.previous)} />
  330. ) : null}
  331. <Button priority="primary" onClick={closeModal}>
  332. {t('Done')}
  333. </Button>
  334. </div>
  335. </ModalActions>
  336. </ModalFooter>
  337. </ModalBody>
  338. );
  339. }
  340. function IOSSendDebugFilesInstruction({
  341. Body: ModalBody,
  342. Header: ModalHeader,
  343. Footer: ModalFooter,
  344. closeModal,
  345. toStep,
  346. step,
  347. }: OnboardingStepProps) {
  348. return (
  349. <ModalBody>
  350. <ModalHeader>
  351. <h3>{t('Set Up Profiling')}</h3>
  352. </ModalHeader>
  353. <p>
  354. {t(`The most straightforward way to provide Sentry with debug information files is to
  355. upload them using sentry-cli. Depending on your workflow, you may want to upload
  356. as part of your build pipeline or when deploying and publishing your application.`)}{' '}
  357. <ExternalLink href="https://docs.sentry.io/product/cli/dif/">
  358. {t('Learn more about Debug Information Files.')}
  359. </ExternalLink>
  360. </p>
  361. <OptionsContainer>
  362. <OptionTitleContainer>
  363. <OptionTitle>{t('Option 1')}</OptionTitle> <Tag>{t('Recommended')}</Tag>
  364. </OptionTitleContainer>
  365. <OptionTitleContainer>
  366. <OptionTitle>{t('Option 2')}</OptionTitle>
  367. </OptionTitleContainer>
  368. </OptionsContainer>
  369. <OptionsContainer>
  370. <Option>
  371. <ExternalOptionTitle href="https://docs.sentry.io/product/cli/dif/#uploading-files">
  372. {t('Sentry-cli')}
  373. <IconOpen />
  374. </ExternalOptionTitle>
  375. <p>{t('Validate and upload debug files using our cli tool.')}</p>
  376. </Option>
  377. <Option>
  378. <ExternalOptionTitle href="https://docs.sentry.io/platforms/apple/dsym/">
  379. {t('Symbol servers')}
  380. <IconOpen />
  381. </ExternalOptionTitle>
  382. <p>
  383. {t('Sentry downloads debug information files from external repositories.')}
  384. </p>
  385. </Option>
  386. </OptionsContainer>
  387. <ModalFooter>
  388. <ModalActions>
  389. <DocsLink />
  390. <div>
  391. <StepIndicator>{t('Step 2 of 2')}</StepIndicator>
  392. {step.previous !== null ? (
  393. <PreviousStepButton onClick={() => toStep(step.previous)} />
  394. ) : null}
  395. <Button priority="primary" onClick={closeModal}>
  396. {t('Next')}
  397. </Button>
  398. </div>
  399. </ModalActions>
  400. </ModalFooter>
  401. </ModalBody>
  402. );
  403. }
  404. type StepButtonProps = Omit<ButtonPropsWithoutAriaLabel, 'children'>;
  405. // A few common component definitions that are used in each step
  406. function NextStepButton(props: StepButtonProps) {
  407. return (
  408. <Button priority="primary" {...props}>
  409. {t('Next')}
  410. </Button>
  411. );
  412. }
  413. function PreviousStepButton(props: StepButtonProps) {
  414. return <Button {...props}>{t('Back')}</Button>;
  415. }
  416. function DocsLink() {
  417. return (
  418. <Button external href="https://docs.sentry.io/">
  419. {t('Read Docs')}
  420. </Button>
  421. );
  422. }
  423. interface ModalActionsProps {
  424. children: React.ReactNode;
  425. }
  426. function ModalActions({children}: ModalActionsProps) {
  427. return <ModalActionsContainer>{children}</ModalActionsContainer>;
  428. }
  429. const OptionTitleContainer = styled('div')`
  430. margin-bottom: ${space(0.5)};
  431. `;
  432. const OptionTitle = styled('span')`
  433. font-weight: bold;
  434. `;
  435. const ExternalOptionTitle = styled(ExternalLink)`
  436. font-weight: bold;
  437. font-size: ${p => p.theme.fontSizeLarge};
  438. display: flex;
  439. align-items: center;
  440. margin-bottom: ${space(0.5)};
  441. svg {
  442. margin-left: ${space(0.5)};
  443. }
  444. `;
  445. const Option = styled('div')`
  446. border-radius: ${p => p.theme.borderRadius};
  447. border: 1px solid ${p => p.theme.border};
  448. padding: ${space(2)};
  449. margin-top: ${space(1)};
  450. `;
  451. const OptionsContainer = styled('div')`
  452. display: grid;
  453. grid-template-columns: 1fr 1fr;
  454. gap: ${space(2)};
  455. > p {
  456. margin: 0;
  457. }
  458. `;
  459. const ModalActionsContainer = styled('div')`
  460. display: flex;
  461. justify-content: space-between;
  462. align-items: center;
  463. flex: 1 1 100%;
  464. button:not(:last-child) {
  465. margin-right: ${space(1)};
  466. }
  467. `;
  468. const StepTitle = styled('div')`
  469. margin-bottom: ${space(1)};
  470. font-weight: bold;
  471. `;
  472. const StepIndicator = styled('span')`
  473. color: ${p => p.theme.subText};
  474. margin-right: ${space(2)};
  475. `;
  476. const PreContainer = styled('pre')`
  477. code {
  478. white-space: pre;
  479. }
  480. `;
  481. function CodeContainer({children}: {children: React.ReactNode}) {
  482. return (
  483. <PreContainer>
  484. <code>{children}</code>
  485. </PreContainer>
  486. );
  487. }