Browse Source

feat(crons): Add quickstart guides (#46008)

Added quickstart guides to the monitor onobarding. Currently added the
ones represented already in the product docs, but can add Laravel and
PHP quickly after.


**Quickstart Guides:**

![image](https://user-images.githubusercontent.com/1421724/228090676-15995a11-c7ef-43ff-9910-0c43295d08b6.png)

![image](https://user-images.githubusercontent.com/1421724/228090504-4923ea9f-70ef-4b2c-b6f4-9dbb919f7107.png)

![image](https://user-images.githubusercontent.com/1421724/228090521-34a4841d-9a4e-4129-9342-d22ecc9c115d.png)

Resolves https://github.com/getsentry/sentry/issues/45675
David Wang 1 year ago
parent
commit
8d5e97c7d0

+ 1 - 0
static/app/components/codeSnippet.tsx

@@ -2,6 +2,7 @@
 // eslint-disable-next-line simple-import-sort/imports
 import Prism from 'prismjs';
 import 'prismjs/components/prism-bash.min';
+import 'prismjs/components/prism-python.min';
 
 import {useEffect, useRef, useState} from 'react';
 import styled from '@emotion/styled';

+ 96 - 0
static/app/views/monitors/components/monitorQuickStartGuide.tsx

@@ -0,0 +1,96 @@
+import {useState} from 'react';
+import styled from '@emotion/styled';
+import partition from 'lodash/partition';
+
+import {CompactSelect} from 'sentry/components/compactSelect';
+import {PlatformKey} from 'sentry/data/platformCategories';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {ProjectKey} from 'sentry/types';
+import {useQuery} from 'sentry/utils/queryClient';
+import {
+  CLICronQuickStart,
+  CurlCronQuickStart,
+  PythonCronQuickStart,
+  QuickStartProps,
+} from 'sentry/views/monitors/components/quickStartEntries';
+
+import {Monitor} from '../types';
+
+interface Props {
+  monitor: Monitor;
+  orgId: string;
+}
+
+interface OnboardingGuide {
+  Guide: React.ComponentType<QuickStartProps>;
+  label: string;
+  platforms?: Set<PlatformKey>;
+}
+
+const onboardingGuides: Record<string, OnboardingGuide> = {
+  cli: {
+    label: 'Sentry CLI',
+    Guide: CLICronQuickStart,
+  },
+  curl: {
+    label: 'cURL',
+    Guide: CurlCronQuickStart,
+  },
+  python: {
+    label: 'Python',
+    Guide: PythonCronQuickStart,
+    platforms: new Set(['python', 'python-celery']),
+  },
+};
+
+const guideToSelectOption = ({key, label}) => ({label, value: key});
+
+export default function MonitorQuickStartGuide({monitor, orgId}: Props) {
+  const {data: projectKeys} = useQuery<Array<ProjectKey>>(
+    [`/projects/${orgId}/${monitor.project.slug}/keys/`],
+    {staleTime: Infinity}
+  );
+
+  const guideList = Object.entries(onboardingGuides).map(([key, guide]) => ({
+    ...guide,
+    key,
+  }));
+
+  const [genericGuides, platformGuides] = partition(
+    guideList,
+    guide => guide.platforms === undefined
+  );
+
+  const exampleOptions = [
+    {label: t('Platform Specfiic'), options: platformGuides.map(guideToSelectOption)},
+    {label: t('Generic'), options: genericGuides.map(guideToSelectOption)},
+  ];
+
+  const platformSpecific = platformGuides.filter(guide =>
+    guide.platforms?.has(monitor.project.platform ?? 'other')
+  );
+
+  const defaultExample = platformSpecific.length > 0 ? platformSpecific[0].key : 'cli';
+
+  const [selectedGuide, setSelectedGuide] = useState(defaultExample);
+  const {Guide} = onboardingGuides[selectedGuide];
+
+  return (
+    <Container>
+      <CompactSelect
+        options={exampleOptions}
+        value={selectedGuide}
+        onChange={({value}) => setSelectedGuide(value)}
+        triggerProps={{prefix: 'Integrate Using'}}
+      />
+      <Guide slug={monitor.slug} orgSlug={orgId} dsnKey={projectKeys?.[0].dsn.public} />
+    </Container>
+  );
+}
+
+const Container = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(2)};
+`;

+ 22 - 14
static/app/views/monitors/components/onboarding.tsx

@@ -1,27 +1,35 @@
 import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
 
-import {Button} from 'sentry/components/button';
+import ExternalLink from 'sentry/components/links/externalLink';
 import OnboardingPanel from 'sentry/components/onboardingPanel';
-import {t} from 'sentry/locale';
+import {t, tct} from 'sentry/locale';
 
-const MonitorOnboarding = () => {
+import {Monitor} from '../types';
+
+import MonitorQuickStartGuide from './monitorQuickStartGuide';
+
+interface Props {
+  monitor: Monitor;
+  orgId: string;
+}
+
+function MonitorOnboarding({orgId, monitor}: Props) {
   return (
     <OnboardingPanel image={<img src={onboardingImg} />}>
-      <h3>{t('Learn how to instrument your cron monitor')}</h3>
+      <h3>{t('Instrument your monitor')}</h3>
       <p>
-        {t(
-          "We'll tell you if this recurring job is running on schedule, failing, or succeeding."
+        {tct(
+          'Select an integration method for your new monitor. For in-depth instructions on integrating Crons, view [docsLink:our complete documentation].',
+          {
+            docsLink: (
+              <ExternalLink href="https://docs.sentry.io/product/crons/getting-started/" />
+            ),
+          }
         )}
       </p>
-      <Button
-        priority="primary"
-        href="https://docs.sentry.io/product/crons/getting-started/#step-2-set-up-health-checks"
-        external
-      >
-        {t('Start Setup')}
-      </Button>
+      <MonitorQuickStartGuide orgId={orgId} monitor={monitor} />
     </OnboardingPanel>
   );
-};
+}
 
 export default MonitorOnboarding;

+ 94 - 0
static/app/views/monitors/components/quickStartEntries.tsx

@@ -0,0 +1,94 @@
+import {Fragment} from 'react';
+import merge from 'lodash/merge';
+
+import {CodeSnippet} from 'sentry/components/codeSnippet';
+import ExternalLink from 'sentry/components/links/externalLink';
+import {t, tct} from 'sentry/locale';
+
+export interface QuickStartProps {
+  dsnKey?: string;
+  orgSlug?: string;
+  slug?: string;
+}
+
+const VALUE_DEFAULTS = {
+  dsnKey: '<my-dsn-key>',
+  orgSlug: '<my-organization-slug>',
+  slug: '<my-monitor-slug>',
+};
+
+function withDefaultProps(props: QuickStartProps): Required<QuickStartProps> {
+  return merge(VALUE_DEFAULTS, props);
+}
+
+export function PythonCronQuickStart(props: QuickStartProps) {
+  const {slug} = withDefaultProps(props);
+
+  const code = `from sentry_sdk.crons import monitor
+
+# Add this decorator to instrument your python function
+@monitor(monitor_slug='${slug}')
+def tell_the_world(msg):
+  print(msg)`;
+
+  return <CodeSnippet language="python">{code}</CodeSnippet>;
+}
+
+export function CLICronQuickStart(props: QuickStartProps) {
+  const {slug, dsnKey} = withDefaultProps(props);
+
+  const script = `# Example for a Python script:
+export SENTRY_DSN=${dsnKey}
+sentry-cli monitors run ${slug} -- python path/to/file`;
+
+  return (
+    <Fragment>
+      <div>
+        {tct(
+          'Make sure to [installLink:install the Sentry CLI] first, then instrument your job like so',
+          {
+            installLink: (
+              <ExternalLink href="https://docs.sentry.io/product/cli/installation/" />
+            ),
+          }
+        )}
+      </div>
+      <CodeSnippet language="bash">{script}</CodeSnippet>
+    </Fragment>
+  );
+}
+
+export function CurlCronQuickStart(props: QuickStartProps) {
+  const {slug, orgSlug, dsnKey} = withDefaultProps(props);
+
+  const checkInSuccessCode = `# Notify Sentry your job is running:
+curl -X POST \\
+    'https://sentry.io/api/0/organizations/${orgSlug}/monitors/${slug}/checkins/' \\
+    --header 'Authorization: DSN ${dsnKey}' \\
+    --header 'Content-Type: application/json' \\
+    --data-raw '{"status": "in_progress"}'
+
+# Execute your scheduled task here...
+
+# Notify Sentry your job has completed successfully:
+curl -X PUT \\
+    'https://sentry.io/api/0/organizations/${orgSlug}/monitors/${slug}/checkins/latest/' \\
+    --header 'Authorization: DSN ${dsnKey}' \\
+    --header 'Content-Type: application/json' \\
+    --data-raw '{"status": "ok"}'`;
+
+  const checkInFailCode = `# Notify Sentry your job has failed:
+curl -X PUT \\
+    'https://sentry.io/api/0/organizations/${orgSlug}/monitors/${slug}/checkins/latest/' \\
+    --header 'Authorization: DSN ${dsnKey}' \\
+    --header 'Content-Type: application/json' \\
+    --data-raw '{"status": "error"}'`;
+
+  return (
+    <Fragment>
+      <CodeSnippet language="bash">{checkInSuccessCode}</CodeSnippet>
+      <div>{t('To notify Sentry if your job execution fails')}</div>
+      <CodeSnippet language="bash">{checkInFailCode}</CodeSnippet>
+    </Fragment>
+  );
+}

+ 1 - 1
static/app/views/monitors/details.tsx

@@ -79,7 +79,7 @@ class MonitorDetails extends AsyncView<Props, State> {
         <Layout.Body>
           <Layout.Main fullWidth>
             {!monitor.lastCheckIn ? (
-              <MonitorOnboarding />
+              <MonitorOnboarding orgId={this.orgSlug} monitor={monitor} />
             ) : (
               <Fragment>
                 <StyledPageFilterBar condensed>