taskConfig.tsx 12 KB

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