import styled from '@emotion/styled'; import {openInviteMembersModal} from 'sentry/actionCreators/modal'; import {navigateTo} from 'sentry/actionCreators/navigation'; import {Client} from 'sentry/api'; import {taskIsDone} from 'sentry/components/onboardingWizard/utils'; import {filterProjects} from 'sentry/components/performanceOnboarding/utils'; import {sourceMaps} from 'sentry/data/platformCategories'; import {t} from 'sentry/locale'; import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator'; import space from 'sentry/styles/space'; import { OnboardingSupplementComponentProps, OnboardingTask, OnboardingTaskDescriptor, OnboardingTaskKey, Organization, Project, } from 'sentry/types'; import EventWaiter from 'sentry/utils/eventWaiter'; import withApi from 'sentry/utils/withApi'; import {OnboardingState} from 'sentry/views/onboarding/targetedOnboarding/types'; import OnboardingProjectsCard from './onboardingCard'; function hasPlatformWithSourceMaps(projects: Project[] | undefined) { return projects !== undefined ? projects.some(({platform}) => platform && sourceMaps.includes(platform)) : false; } type FirstEventWaiterProps = OnboardingSupplementComponentProps & { api: Client; }; type Options = { /** * The organization to show onboarding tasks for */ organization: Organization; onboardingState?: OnboardingState; /** * A list of the organizations projects. This is used for some onboarding * tasks to show additional task details (such as for suggesting sourcemaps) */ projects?: Project[]; }; function getIssueAlertUrl({projects, organization}: Options) { if (!projects || !projects.length) { return `/organizations/${organization.slug}/alerts/rules/`; } // pick the first project with events if we have that, otherwise just pick the first project const firstProjectWithEvents = projects.find(project => !!project.firstEvent); const project = firstProjectWithEvents ?? projects[0]; return `/organizations/${organization.slug}/alerts/${project.slug}/wizard/`; } function getMetricAlertUrl({projects, organization}: Options) { if (!projects || !projects.length) { return `/organizations/${organization.slug}/alerts/rules/`; } // pick the first project with transaction events if we have that, otherwise just pick the first project const firstProjectWithEvents = projects.find( project => !!project.firstTransactionEvent ); const project = firstProjectWithEvents ?? projects[0]; return `/organizations/${organization.slug}/alerts/${project.slug}/wizard/?alert_option=trans_duration`; } export function getOnboardingTasks({ organization, projects, onboardingState, }: Options): OnboardingTaskDescriptor[] { return [ { task: OnboardingTaskKey.FIRST_PROJECT, title: t('Create a project'), description: t( "Monitor in seconds by adding a simple lines of code to your project. It's as easy as microwaving leftover pizza." ), skippable: false, requisites: [], actionType: 'app', location: `/organizations/${organization.slug}/projects/new/`, display: true, }, { task: OnboardingTaskKey.FIRST_EVENT, title: t('Capture your first error'), description: t( "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." ), skippable: false, requisites: [OnboardingTaskKey.FIRST_PROJECT], actionType: 'app', location: `/settings/${organization.slug}/projects/:projectId/install/`, display: true, SupplementComponent: withApi(({api, task, onCompleteTask}: FirstEventWaiterProps) => !!projects?.length && task.requisiteTasks.length === 0 && !task.completionSeen ? ( !taskIsDone(task) && onCompleteTask()} > {() => } ) : null ), }, { task: OnboardingTaskKey.INVITE_MEMBER, title: t('Invite your team'), description: t( 'Assign issues and comment on shared errors with coworkers so you always know who to blame when sh*t hits the fan.' ), skippable: true, requisites: [], actionType: 'action', action: () => openInviteMembersModal({source: 'onboarding_widget'}), display: true, }, { task: OnboardingTaskKey.FIRST_INTEGRATION, title: t('Install any of our 40+ integrations'), description: t( 'Get alerted in Slack. Two-way sync issues between Sentry and Jira. Notify Sentry of releases from GitHub, Vercel, or Netlify.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/settings/${organization.slug}/integrations/`, display: true, }, { task: OnboardingTaskKey.SECOND_PLATFORM, title: t('Create another project'), description: t( '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.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/organizations/${organization.slug}/projects/new/`, display: true, }, { task: OnboardingTaskKey.FIRST_TRANSACTION, title: t('Boost performance'), description: t( "Don't keep users waiting. Trace transactions, investigate spans and cross-reference related issues for those mission-critical endpoints." ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT], actionType: 'action', action: ({router}) => { if (!organization.features?.includes('performance-onboarding-checklist')) { window.open( 'https://docs.sentry.io/product/performance/getting-started/', '_blank' ); return; } // TODO: add analytics here for this specific action. if (!projects) { navigateTo(`/organizations/${organization.slug}/performance/`, router); return; } const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} = filterProjects(projects); if (projectsWithoutFirstTransactionEvent.length <= 0) { navigateTo(`/organizations/${organization.slug}/performance/`, router); return; } if (projectsForOnboarding.length) { navigateTo( `/organizations/${organization.slug}/performance/?project=${projectsForOnboarding[0].id}#performance-sidequest`, router ); return; } navigateTo( `/organizations/${organization.slug}/performance/?project=${projectsWithoutFirstTransactionEvent[0].id}#performance-sidequest`, router ); }, display: true, SupplementComponent: withApi(({api, task, onCompleteTask}: FirstEventWaiterProps) => !!projects?.length && task.requisiteTasks.length === 0 && !task.completionSeen ? ( !taskIsDone(task) && onCompleteTask()} > {() => } ) : null ), }, { task: OnboardingTaskKey.USER_CONTEXT, title: t('Get more user context'), description: t( '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.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'external', location: 'https://docs.sentry.io/platform-redirect/?next=/enriching-events/identify-user/', display: true, }, { task: OnboardingTaskKey.RELEASE_TRACKING, title: t('Track releases'), description: t( 'Take an in-depth look at the health of each and every release with crash analytics, errors, related issues and suspect commits.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/settings/${organization.slug}/projects/:projectId/release-tracking/`, display: true, }, { task: OnboardingTaskKey.SOURCEMAPS, title: t('Upload source maps'), description: t( "Deminify Javascript source code to debug with context. Seeing code in it's original form will help you debunk the ghosts of errors past." ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'external', location: 'https://docs.sentry.io/platforms/javascript/sourcemaps/', display: hasPlatformWithSourceMaps(projects), }, { task: OnboardingTaskKey.USER_REPORTS, title: 'User crash reports', description: t('Collect user feedback when your application crashes'), skippable: true, requisites: [ OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT, OnboardingTaskKey.USER_CONTEXT, ], actionType: 'app', location: `/settings/${organization.slug}/projects/:projectId/user-reports/`, display: false, }, { task: OnboardingTaskKey.ISSUE_TRACKER, title: t('Set up issue tracking'), description: t('Link to Sentry issues within your issue tracker'), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/settings/${organization.slug}/projects/:projectId/plugins/`, display: false, }, { task: OnboardingTaskKey.ALERT_RULE, title: t('Configure an Issue Alert'), description: t( 'We all have issues. Get real-time error notifications by setting up alerts for issues that match your set criteria.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT], actionType: 'app', location: getIssueAlertUrl({projects, organization, onboardingState}), display: true, }, { task: OnboardingTaskKey.METRIC_ALERT, title: t('Create a Performance Alert'), description: t( '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.' ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_TRANSACTION], actionType: 'app', location: getMetricAlertUrl({projects, organization, onboardingState}), display: organization.features?.includes('incidents'), }, { task: OnboardingTaskKey.USER_SELECTED_PROJECTS, title: t('Projects to Setup'), description: '', skippable: true, requisites: [], actionType: 'action', action: () => {}, display: true, renderCard: OnboardingProjectsCard, }, ]; } export function getMergedTasks({organization, projects, onboardingState}: Options) { const taskDescriptors = getOnboardingTasks({organization, projects, onboardingState}); const serverTasks = organization.onboardingTasks; // Map server task state (i.e. completed status) with tasks objects const allTasks = taskDescriptors.map( desc => ({ ...desc, ...serverTasks.find( serverTask => serverTask.task === desc.task || serverTask.task === desc.serverTask ), requisiteTasks: [], } as OnboardingTask) ); // Map incomplete requisiteTasks as full task objects return allTasks.map(task => ({ ...task, requisiteTasks: task.requisites .map(key => allTasks.find(task2 => task2.task === key)!) .filter(reqTask => reqTask.status !== 'complete'), })); } const PulsingIndicator = styled('div')` ${pulsingIndicatorStyles}; margin-right: ${space(1)}; `; const EventWaitingIndicator = styled((p: React.HTMLAttributes) => (
{t('Waiting for event')}
))` display: flex; align-items: center; flex-grow: 1; font-size: ${p => p.theme.fontSizeMedium}; color: ${p => p.theme.pink300}; `;