taskConfig.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import styled from '@emotion/styled';
  2. import {openInviteMembersModal} from 'sentry/actionCreators/modal';
  3. import {navigateTo} from 'sentry/actionCreators/navigation';
  4. import {Client} from 'sentry/api';
  5. import {OnboardingContextProps} from 'sentry/components/onboarding/onboardingContext';
  6. import {taskIsDone} from 'sentry/components/onboardingWizard/utils';
  7. import {filterProjects} from 'sentry/components/performanceOnboarding/utils';
  8. import {sourceMaps} from 'sentry/data/platformCategories';
  9. import {t} from 'sentry/locale';
  10. import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
  11. import {space} from 'sentry/styles/space';
  12. import {
  13. OnboardingSupplementComponentProps,
  14. OnboardingTask,
  15. OnboardingTaskDescriptor,
  16. OnboardingTaskKey,
  17. Organization,
  18. Project,
  19. } from 'sentry/types';
  20. import {isDemoWalkthrough} from 'sentry/utils/demoMode';
  21. import EventWaiter from 'sentry/utils/eventWaiter';
  22. import withApi from 'sentry/utils/withApi';
  23. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  24. import OnboardingProjectsCard from './onboardingProjectsCard';
  25. function hasPlatformWithSourceMaps(projects: Project[] | undefined) {
  26. return projects !== undefined
  27. ? projects.some(({platform}) => platform && sourceMaps.includes(platform))
  28. : false;
  29. }
  30. type FirstEventWaiterProps = OnboardingSupplementComponentProps & {
  31. api: Client;
  32. };
  33. type Options = {
  34. /**
  35. * The organization to show onboarding tasks for
  36. */
  37. organization: Organization;
  38. onboardingContext?: OnboardingContextProps;
  39. /**
  40. * A list of the organizations projects. This is used for some onboarding
  41. * tasks to show additional task details (such as for suggesting sourcemaps)
  42. */
  43. projects?: Project[];
  44. };
  45. function getIssueAlertUrl({projects, organization}: Options) {
  46. if (!projects || !projects.length) {
  47. return `/organizations/${organization.slug}/alerts/rules/`;
  48. }
  49. // pick the first project with events if we have that, otherwise just pick the first project
  50. const firstProjectWithEvents = projects.find(project => !!project.firstEvent);
  51. const project = firstProjectWithEvents ?? projects[0];
  52. return `/organizations/${organization.slug}/alerts/${project.slug}/wizard/`;
  53. }
  54. function getOnboardingInstructionsUrl({projects, organization}: Options) {
  55. // This shall never be the case, since this is step is locked until a project is created,
  56. // but if the user falls into this case for some reason,
  57. // he needs to select the platform again since it is not available as a parameter here
  58. if (!projects || !projects.length) {
  59. return `/getting-started/:projectId/`;
  60. }
  61. const allProjectsWithoutErrors = projects.every(project => !project.firstEvent);
  62. // If all created projects don't have any errors,
  63. // we ask the user to pick a project before navigating to the instructions
  64. if (allProjectsWithoutErrors) {
  65. return `/getting-started/:projectId/`;
  66. }
  67. // Pick the first project without an error
  68. const firstProjectWithoutError = projects.find(project => !project.firstEvent);
  69. // If all projects contain errors, this step will not be visible to the user,
  70. // but if the user falls into this case for some reason, we pick the first project
  71. const project = firstProjectWithoutError ?? projects[0];
  72. let url = `/${organization.slug}/${project.slug}/getting-started/`;
  73. if (project.platform) {
  74. url = url + `${project.platform}/`;
  75. }
  76. return url;
  77. }
  78. function getMetricAlertUrl({projects, organization}: Options) {
  79. if (!projects || !projects.length) {
  80. return `/organizations/${organization.slug}/alerts/rules/`;
  81. }
  82. // pick the first project with transaction events if we have that, otherwise just pick the first project
  83. const firstProjectWithEvents = projects.find(
  84. project => !!project.firstTransactionEvent
  85. );
  86. const project = firstProjectWithEvents ?? projects[0];
  87. return `/organizations/${organization.slug}/alerts/${project.slug}/wizard/?alert_option=trans_duration`;
  88. }
  89. export function getOnboardingTasks({
  90. organization,
  91. projects,
  92. onboardingContext,
  93. }: Options): OnboardingTaskDescriptor[] {
  94. if (isDemoWalkthrough()) {
  95. return [
  96. {
  97. task: OnboardingTaskKey.ISSUE_GUIDE,
  98. title: t('Issues'),
  99. description: t(
  100. 'Here’s a list of errors and performance problems. And everything you need to know to fix it.'
  101. ),
  102. skippable: false,
  103. requisites: [],
  104. actionType: 'app',
  105. location: `/organizations/${organization.slug}/issues/`,
  106. display: true,
  107. },
  108. {
  109. task: OnboardingTaskKey.PERFORMANCE_GUIDE,
  110. title: t('Performance'),
  111. description: t(
  112. 'See slow fast. Trace slow-loading pages back to their API calls as well as all related errors'
  113. ),
  114. skippable: false,
  115. requisites: [],
  116. actionType: 'app',
  117. location: `/organizations/${organization.slug}/performance/`,
  118. display: true,
  119. },
  120. {
  121. task: OnboardingTaskKey.RELEASE_GUIDE,
  122. title: t('Releases'),
  123. description: t(
  124. 'Track the health of every release. See differences between releases from crash analytics to adoption rates.'
  125. ),
  126. skippable: false,
  127. requisites: [],
  128. actionType: 'app',
  129. location: `/organizations/${organization.slug}/releases/`,
  130. display: true,
  131. },
  132. {
  133. task: OnboardingTaskKey.SIDEBAR_GUIDE,
  134. title: t('Check out the different tabs'),
  135. description: t('Press the start button for a guided tour through each tab.'),
  136. skippable: false,
  137. requisites: [],
  138. actionType: 'app',
  139. location: `/organizations/${organization.slug}/projects/`,
  140. display: true,
  141. },
  142. ];
  143. }
  144. return [
  145. {
  146. task: OnboardingTaskKey.FIRST_PROJECT,
  147. title: t('Create a project'),
  148. description: t(
  149. "Monitor in seconds by adding a simple lines of code to your project. It's as easy as microwaving leftover pizza."
  150. ),
  151. skippable: false,
  152. requisites: [],
  153. actionType: 'app',
  154. location: `/organizations/${organization.slug}/projects/new/`,
  155. display: true,
  156. },
  157. {
  158. task: OnboardingTaskKey.FIRST_EVENT,
  159. title: t('Capture your first error'),
  160. description: t(
  161. "Time to test it out. Now that you've created a project, capture your first error. We've got an example you can fiddle with."
  162. ),
  163. skippable: false,
  164. requisites: [OnboardingTaskKey.FIRST_PROJECT],
  165. actionType: 'app',
  166. location: getOnboardingInstructionsUrl({projects, organization}),
  167. display: true,
  168. SupplementComponent: withApi(
  169. ({api, task, onCompleteTask}: FirstEventWaiterProps) =>
  170. !!projects?.length &&
  171. task.requisiteTasks.length === 0 &&
  172. !task.completionSeen ? (
  173. <EventWaiter
  174. api={api}
  175. organization={organization}
  176. project={projects[0]}
  177. eventType="error"
  178. onIssueReceived={() => !taskIsDone(task) && onCompleteTask()}
  179. >
  180. {() => <EventWaitingIndicator />}
  181. </EventWaiter>
  182. ) : null
  183. ),
  184. },
  185. {
  186. task: OnboardingTaskKey.INVITE_MEMBER,
  187. title: t('Invite your team'),
  188. description: t(
  189. 'Assign issues and comment on shared errors with coworkers so you always know who to blame when sh*t hits the fan.'
  190. ),
  191. skippable: true,
  192. requisites: [],
  193. actionType: 'action',
  194. action: () => openInviteMembersModal({source: 'onboarding_widget'}),
  195. display: true,
  196. },
  197. {
  198. task: OnboardingTaskKey.FIRST_INTEGRATION,
  199. title: t('Install any of our 40+ integrations'),
  200. description: t(
  201. 'Get alerted in Slack. Two-way sync issues between Sentry and Jira. Notify Sentry of releases from GitHub, Vercel, or Netlify.'
  202. ),
  203. skippable: true,
  204. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  205. actionType: 'app',
  206. location: `/settings/${organization.slug}/integrations/`,
  207. display: true,
  208. },
  209. {
  210. task: OnboardingTaskKey.SECOND_PLATFORM,
  211. title: t('Create another project'),
  212. description: t(
  213. 'Easy, right? Don’t stop at one. Set up another project and send it events to keep things running smoothly in both the frontend and backend.'
  214. ),
  215. skippable: true,
  216. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  217. actionType: 'app',
  218. location: `/organizations/${organization.slug}/projects/new/`,
  219. display: true,
  220. },
  221. {
  222. task: OnboardingTaskKey.FIRST_TRANSACTION,
  223. title: t('Boost performance'),
  224. description: t(
  225. "Don't keep users waiting. Trace transactions, investigate spans and cross-reference related issues for those mission-critical endpoints."
  226. ),
  227. skippable: true,
  228. requisites: [OnboardingTaskKey.FIRST_PROJECT],
  229. actionType: 'action',
  230. action: ({router}) => {
  231. // Use `features?.` because getsentry has a different `Organization` type/payload
  232. if (!organization.features?.includes('performance-onboarding-checklist')) {
  233. window.open(
  234. 'https://docs.sentry.io/product/performance/getting-started/',
  235. '_blank'
  236. );
  237. return;
  238. }
  239. // TODO: add analytics here for this specific action.
  240. if (!projects) {
  241. navigateTo(`/organizations/${organization.slug}/performance/`, router);
  242. return;
  243. }
  244. const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} =
  245. filterProjects(projects);
  246. if (projectsWithoutFirstTransactionEvent.length <= 0) {
  247. navigateTo(`/organizations/${organization.slug}/performance/`, router);
  248. return;
  249. }
  250. if (projectsForOnboarding.length) {
  251. navigateTo(
  252. `/organizations/${organization.slug}/performance/?project=${projectsForOnboarding[0].id}#performance-sidequest`,
  253. router
  254. );
  255. return;
  256. }
  257. navigateTo(
  258. `/organizations/${organization.slug}/performance/?project=${projectsWithoutFirstTransactionEvent[0].id}#performance-sidequest`,
  259. router
  260. );
  261. },
  262. display: true,
  263. SupplementComponent: withApi(
  264. ({api, task, onCompleteTask}: FirstEventWaiterProps) =>
  265. !!projects?.length &&
  266. task.requisiteTasks.length === 0 &&
  267. !task.completionSeen ? (
  268. <EventWaiter
  269. api={api}
  270. organization={organization}
  271. project={projects[0]}
  272. eventType="transaction"
  273. onIssueReceived={() => !taskIsDone(task) && onCompleteTask()}
  274. >
  275. {() => <EventWaitingIndicator />}
  276. </EventWaiter>
  277. ) : null
  278. ),
  279. },
  280. {
  281. task: OnboardingTaskKey.USER_CONTEXT,
  282. title: t('Get more user context'),
  283. description: t(
  284. 'Enable us to pinpoint which users are suffering from that bad code, so you can debug the problem more swiftly and maybe even apologize for it.'
  285. ),
  286. skippable: true,
  287. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  288. actionType: 'external',
  289. location:
  290. 'https://docs.sentry.io/platform-redirect/?next=/enriching-events/identify-user/',
  291. display: true,
  292. },
  293. {
  294. task: OnboardingTaskKey.SESSION_REPLAY,
  295. title: t('See a video-like reproduction'),
  296. description: t(
  297. 'Get to the root cause of error or latency issues faster by seeing all the technical details related to those issues in video-like reproductions of your user sessions.'
  298. ),
  299. skippable: true,
  300. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  301. actionType: 'app',
  302. location: normalizeUrl(
  303. `/organizations/${organization.slug}/replays/#replay-sidequest`
  304. ),
  305. display: organization.features?.includes('session-replay'),
  306. SupplementComponent: withApi(
  307. ({api, task, onCompleteTask}: FirstEventWaiterProps) =>
  308. !!projects?.length &&
  309. task.requisiteTasks.length === 0 &&
  310. !task.completionSeen ? (
  311. <EventWaiter
  312. api={api}
  313. organization={organization}
  314. project={projects[0]}
  315. eventType="replay"
  316. onIssueReceived={() => !taskIsDone(task) && onCompleteTask()}
  317. >
  318. {() => <EventWaitingIndicator text={t('Waiting for user session')} />}
  319. </EventWaiter>
  320. ) : null
  321. ),
  322. },
  323. {
  324. task: OnboardingTaskKey.RELEASE_TRACKING,
  325. title: t('Track releases'),
  326. description: t(
  327. 'Take an in-depth look at the health of each and every release with crash analytics, errors, related issues and suspect commits.'
  328. ),
  329. skippable: true,
  330. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  331. actionType: 'app',
  332. location: `/settings/${organization.slug}/projects/:projectId/release-tracking/`,
  333. display: true,
  334. },
  335. {
  336. task: OnboardingTaskKey.SOURCEMAPS,
  337. title: t('Upload source maps'),
  338. description: t(
  339. "Deminify Javascript source code to debug with context. Seeing code in it's original form will help you debunk the ghosts of errors past."
  340. ),
  341. skippable: true,
  342. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  343. actionType: 'external',
  344. location: 'https://docs.sentry.io/platforms/javascript/sourcemaps/',
  345. display: hasPlatformWithSourceMaps(projects),
  346. },
  347. {
  348. task: OnboardingTaskKey.USER_REPORTS,
  349. title: 'User crash reports',
  350. description: t('Collect user feedback when your application crashes'),
  351. skippable: true,
  352. requisites: [
  353. OnboardingTaskKey.FIRST_PROJECT,
  354. OnboardingTaskKey.FIRST_EVENT,
  355. OnboardingTaskKey.USER_CONTEXT,
  356. ],
  357. actionType: 'app',
  358. location: `/settings/${organization.slug}/projects/:projectId/user-reports/`,
  359. display: false,
  360. },
  361. {
  362. task: OnboardingTaskKey.ISSUE_TRACKER,
  363. title: t('Set up issue tracking'),
  364. description: t('Link to Sentry issues within your issue tracker'),
  365. skippable: true,
  366. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
  367. actionType: 'app',
  368. location: `/settings/${organization.slug}/projects/:projectId/plugins/`,
  369. display: false,
  370. },
  371. {
  372. task: OnboardingTaskKey.ALERT_RULE,
  373. title: t('Configure an Issue Alert'),
  374. description: t(
  375. 'We all have issues. Get real-time error notifications by setting up alerts for issues that match your set criteria.'
  376. ),
  377. skippable: true,
  378. requisites: [OnboardingTaskKey.FIRST_PROJECT],
  379. actionType: 'app',
  380. location: getIssueAlertUrl({projects, organization, onboardingContext}),
  381. display: true,
  382. },
  383. {
  384. task: OnboardingTaskKey.METRIC_ALERT,
  385. title: t('Create a Performance Alert'),
  386. description: t(
  387. 'See slow fast with performance alerts. Set up alerts for notifications about slow page load times, API latency, or when throughput significantly deviates from normal.'
  388. ),
  389. skippable: true,
  390. requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_TRANSACTION],
  391. actionType: 'app',
  392. location: getMetricAlertUrl({projects, organization, onboardingContext}),
  393. // Use `features?.` because getsentry has a different `Organization` type/payload
  394. display: organization.features?.includes('incidents'),
  395. },
  396. {
  397. task: OnboardingTaskKey.USER_SELECTED_PROJECTS,
  398. title: t('Projects to Setup'),
  399. description: '',
  400. skippable: true,
  401. requisites: [],
  402. actionType: 'action',
  403. action: () => {},
  404. display: true,
  405. renderCard: OnboardingProjectsCard,
  406. },
  407. ];
  408. }
  409. export function getMergedTasks({organization, projects, onboardingContext}: Options) {
  410. const taskDescriptors = getOnboardingTasks({organization, projects, onboardingContext});
  411. const serverTasks = organization.onboardingTasks;
  412. // Map server task state (i.e. completed status) with tasks objects
  413. const allTasks = taskDescriptors.map(
  414. desc =>
  415. ({
  416. ...desc,
  417. ...serverTasks.find(
  418. serverTask =>
  419. serverTask.task === desc.task || serverTask.task === desc.serverTask
  420. ),
  421. requisiteTasks: [],
  422. }) as OnboardingTask
  423. );
  424. // Map incomplete requisiteTasks as full task objects
  425. return allTasks.map(task => ({
  426. ...task,
  427. requisiteTasks: task.requisites
  428. .map(key => allTasks.find(task2 => task2.task === key)!)
  429. .filter(reqTask => reqTask.status !== 'complete'),
  430. }));
  431. }
  432. const PulsingIndicator = styled('div')`
  433. ${pulsingIndicatorStyles};
  434. margin-right: ${space(1)};
  435. `;
  436. const EventWaitingIndicator = styled(
  437. (p: React.HTMLAttributes<HTMLDivElement> & {text?: string}) => (
  438. <div {...p}>
  439. <PulsingIndicator />
  440. {p.text || t('Waiting for event')}
  441. </div>
  442. )
  443. )`
  444. display: flex;
  445. align-items: center;
  446. flex-grow: 1;
  447. font-size: ${p => p.theme.fontSizeMedium};
  448. color: ${p => p.theme.pink400};
  449. `;