taskConfig.tsx 11 KB

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