taskConfig.tsx 15 KB

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