taskConfig.tsx 16 KB

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