Browse Source

feat(onboarding): Onboarding platform integration for aws lambda (#38917)

This PR enables the automatic installation of AWS Lambda platform during
onboarding.

![image](https://user-images.githubusercontent.com/8980455/190494659-beb8fe99-aeb1-4068-823e-caf229759884.png)
Zhixing Zhang 2 years ago
parent
commit
d196abe960

+ 86 - 130
static/app/views/onboarding/integrationSetup.tsx

@@ -1,121 +1,90 @@
 import 'prism-sentry/index.css';
 
-import {Component, Fragment} from 'react';
+import {Fragment, useCallback, useEffect, useState} from 'react';
 import styled from '@emotion/styled';
 import {motion} from 'framer-motion';
 
 import {openInviteMembersModal} from 'sentry/actionCreators/modal';
-import {Client} from 'sentry/api';
 import Alert from 'sentry/components/alert';
 import Button from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import LoadingError from 'sentry/components/loadingError';
-import {PlatformKey} from 'sentry/data/platformCategories';
 import platforms from 'sentry/data/platforms';
 import {t, tct} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {IntegrationProvider, Organization} from 'sentry/types';
+import {IntegrationProvider, Project} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import getDynamicText from 'sentry/utils/getDynamicText';
 import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil';
-import withApi from 'sentry/utils/withApi';
-import withOrganization from 'sentry/utils/withOrganization';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
 import {AddIntegrationButton} from 'sentry/views/organizationIntegrations/addIntegrationButton';
 
 import FirstEventFooter from './components/firstEventFooter';
 import AddInstallationInstructions from './components/integrations/addInstallationInstructions';
 import PostInstallCodeSnippet from './components/integrations/postInstallCodeSnippet';
 import SetupIntroduction from './components/setupIntroduction';
-import {StepProps} from './types';
 
-type Props = StepProps & {
-  api: Client;
+type Props = {
   integrationSlug: string;
-  organization: Organization;
+  project: Project | null;
+  onClickManualSetup?: () => void;
 };
 
-type State = {
-  hasError: boolean;
-  installed: boolean;
-  loadedPlatform: PlatformKey | null;
-  provider: IntegrationProvider | null;
-};
-
-class IntegrationSetup extends Component<Props, State> {
-  state: State = {
-    loadedPlatform: null,
-    hasError: false,
-    provider: null,
-    installed: false,
-  };
-
-  componentDidMount() {
-    this.fetchData();
-  }
+function IntegrationSetup(props: Props) {
+  const [hasError, setHasError] = useState(false);
+  const [installed, setInstalled] = useState(false);
+  const [provider, setProvider] = useState<IntegrationProvider | null>(null);
 
-  componentDidUpdate(nextProps: Props) {
-    if (
-      nextProps.platform !== this.props.platform ||
-      nextProps.project !== this.props.project
-    ) {
-      this.fetchData();
-    }
-  }
+  const organization = useOrganization();
 
-  get manualSetupUrl() {
-    const {search} = window.location;
-    // honor any existing query params
-    const separator = search.includes('?') ? '&' : '?';
-    return `${search}${separator}manual=1`;
-  }
-
-  get platformDocs() {
-    // TODO: make dynamic based on the integration
-    return 'https://docs.sentry.io/product/integrations/cloud-monitoring/aws-lambda/';
-  }
-
-  fetchData = async () => {
-    const {api, organization, platform, integrationSlug} = this.props;
+  const {project, integrationSlug} = props;
 
+  const api = useApi();
+  const fetchData = useCallback(() => {
     if (!integrationSlug) {
-      return;
+      return Promise.resolve();
     }
 
-    try {
-      const endpoint = `/organizations/${organization.slug}/config/integrations/?provider_key=${integrationSlug}`;
-      const integrations = await api.requestPromise(endpoint);
-      const provider = integrations.providers[0];
-
-      this.setState({provider, loadedPlatform: platform, hasError: false});
-    } catch (error) {
-      this.setState({hasError: error});
-      throw error;
-    }
-  };
-
-  handleFullDocsClick = () => {
-    const {organization} = this.props;
+    const endpoint = `/organizations/${organization.slug}/config/integrations/?provider_key=${integrationSlug}`;
+    return api
+      .requestPromise(endpoint)
+      .then(integrations => {
+        setProvider(integrations.providers[0]);
+        setHasError(false);
+      })
+      .catch(error => {
+        setHasError(true);
+        throw error;
+      });
+  }, [integrationSlug, api, organization.slug]);
+
+  useEffect(() => {
+    fetchData();
+  }, [fetchData]);
+
+  const loadingError = (
+    <LoadingError
+      message={t(
+        'Failed to load the integration for the %s platform.',
+        project?.platform ?? 'other'
+      )}
+      onRetry={fetchData}
+    />
+  );
+
+  const testOnlyAlert = (
+    <Alert type="warning">
+      Platform documentation is not rendered in for tests in CI
+    </Alert>
+  );
+
+  const handleFullDocsClick = () => {
     trackAdvancedAnalyticsEvent('growth.onboarding_view_full_docs', {organization});
   };
 
-  trackSwitchToManual = () => {
-    const {organization, integrationSlug} = this.props;
-    trackIntegrationAnalytics('integrations.switch_manual_sdk_setup', {
-      integration_type: 'first_party',
-      integration: integrationSlug,
-      view: 'onboarding',
-      organization,
-    });
-  };
-
-  handleAddIntegration = () => {
-    this.setState({installed: true});
-  };
-
-  renderSetupInstructions = () => {
-    const {platform} = this.props;
-    const {loadedPlatform} = this.state;
-    const currentPlatform = loadedPlatform ?? platform ?? 'other';
+  const renderSetupInstructions = () => {
+    const currentPlatform = project?.platform ?? 'other';
     return (
       <SetupIntroduction
         stepHeaderText={t(
@@ -126,17 +95,14 @@ class IntegrationSetup extends Component<Props, State> {
       />
     );
   };
-
-  renderIntegrationInstructions() {
-    const {organization, project} = this.props;
-    const {provider} = this.state;
+  const renderIntegrationInstructions = () => {
     if (!provider || !project) {
       return null;
     }
 
     return (
       <Fragment>
-        {this.renderSetupInstructions()}
+        {renderSetupInstructions()}
         <motion.p
           variants={{
             initial: {opacity: 0},
@@ -173,7 +139,7 @@ class IntegrationSetup extends Component<Props, State> {
           <StyledButtonBar gap={1}>
             <AddIntegrationButton
               provider={provider}
-              onAddIntegration={this.handleAddIntegration}
+              onAddIntegration={() => setInstalled(true)}
               organization={organization}
               priority="primary"
               size="sm"
@@ -186,7 +152,15 @@ class IntegrationSetup extends Component<Props, State> {
                 pathname: window.location.pathname,
                 query: {manual: '1'},
               }}
-              onClick={this.trackSwitchToManual}
+              onClick={() => {
+                props.onClickManualSetup?.();
+                trackIntegrationAnalytics('integrations.switch_manual_sdk_setup', {
+                  integration_type: 'first_party',
+                  integration: integrationSlug,
+                  view: 'onboarding',
+                  organization,
+                });
+              }}
             >
               {t('Manual Setup')}
             </Button>
@@ -194,57 +168,39 @@ class IntegrationSetup extends Component<Props, State> {
         </DocsWrapper>
       </Fragment>
     );
-  }
+  };
 
-  renderPostInstallInstructions() {
-    const {organization, project, platform} = this.props;
-    const {provider} = this.state;
-    if (!project || !provider || !platform) {
+  const renderPostInstallInstructions = () => {
+    if (!project || !provider) {
       return null;
     }
     return (
       <Fragment>
-        {this.renderSetupInstructions()}
-        <PostInstallCodeSnippet provider={provider} platform={platform} isOnboarding />
+        {renderSetupInstructions()}
+        <PostInstallCodeSnippet
+          provider={provider}
+          platform={project.platform}
+          isOnboarding
+        />
         <FirstEventFooter
           project={project}
           organization={organization}
-          docsLink={this.platformDocs}
-          docsOnClick={this.handleFullDocsClick}
+          docsLink="https://docs.sentry.io/product/integrations/cloud-monitoring/aws-lambda/" // TODO: make dynamic based on the integration
+          docsOnClick={handleFullDocsClick}
         />
       </Fragment>
     );
-  }
-
-  render() {
-    const {platform} = this.props;
-    const {hasError} = this.state;
-
-    const loadingError = (
-      <LoadingError
-        message={t('Failed to load the integration for the %s platform.', platform)}
-        onRetry={this.fetchData}
-      />
-    );
-
-    const testOnlyAlert = (
-      <Alert type="warning">
-        Platform documentation is not rendered in for tests in CI
-      </Alert>
-    );
+  };
 
-    return (
-      <Fragment>
-        {this.state.installed
-          ? this.renderPostInstallInstructions()
-          : this.renderIntegrationInstructions()}
-        {getDynamicText({
-          value: !hasError ? null : loadingError,
-          fixed: testOnlyAlert,
-        })}
-      </Fragment>
-    );
-  }
+  return (
+    <Fragment>
+      {installed ? renderPostInstallInstructions() : renderIntegrationInstructions()}
+      {getDynamicText({
+        value: !hasError ? null : loadingError,
+        fixed: testOnlyAlert,
+      })}
+    </Fragment>
+  );
 }
 
 const DocsWrapper = styled(motion.div)``;
@@ -266,4 +222,4 @@ const StyledButtonBar = styled(ButtonBar)`
   }
 `;
 
-export default withOrganization(withApi(IntegrationSetup));
+export default IntegrationSetup;

+ 35 - 9
static/app/views/onboarding/targetedOnboarding/setupDocs.tsx

@@ -21,10 +21,13 @@ import {Organization, Project} from 'sentry/types';
 import {logExperiment} from 'sentry/utils/analytics';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import getDynamicText from 'sentry/utils/getDynamicText';
+import {platformToIntegrationMap} from 'sentry/utils/integrationUtil';
 import {Theme} from 'sentry/utils/theme';
 import useApi from 'sentry/utils/useApi';
 import withProjects from 'sentry/utils/withProjects';
 
+import IntegrationSetup from '../integrationSetup';
+
 import FirstEventFooter from './components/firstEventFooter';
 import FullIntroduction from './components/fullIntroduction';
 import ProjectSidebarSection from './components/projectSidebarSection';
@@ -230,6 +233,9 @@ function SetupDocs({
     (p, i) => i > projectIndex && !checkProjectHasFirstEvent(p)
   );
 
+  const integrationSlug = project?.platform && platformToIntegrationMap[project.platform];
+  const [integrationUseManualSetup, setIntegrationUseManualSetup] = useState(false);
+
   useEffect(() => {
     // should not redirect if we don't have an active client state or projects aren't loaded
     if (!clientState || loadingProjects) {
@@ -250,6 +256,12 @@ function SetupDocs({
     if (!project?.platform) {
       return;
     }
+    if (integrationSlug && !integrationUseManualSetup) {
+      setLoadedPlatform(project.platform);
+      setPlatformDocs(null);
+      setHasError(false);
+      return;
+    }
     try {
       const loadedDocs = await loadDocs(
         api,
@@ -264,7 +276,7 @@ function SetupDocs({
       setHasError(error);
       throw error;
     }
-  }, [project, api, organization]);
+  }, [project, api, organization, integrationSlug, integrationUseManualSetup]);
 
   useEffect(() => {
     fetchData();
@@ -275,6 +287,10 @@ function SetupDocs({
   }
 
   const setNewProject = (newProjectId: string) => {
+    setLoadedPlatform(null);
+    setPlatformDocs(null);
+    setHasError(false);
+    setIntegrationUseManualSetup(false);
     const searchParams = new URLSearchParams({
       sub_step: 'project',
       project_id: newProjectId,
@@ -314,14 +330,24 @@ function SetupDocs({
           />
         </SidebarWrapper>
         <MainContent>
-          <ProjecDocs
-            platform={loadedPlatform}
-            organization={organization}
-            project={project}
-            hasError={hasError}
-            platformDocs={platformDocs}
-            onRetry={fetchData}
-          />
+          {integrationSlug && !integrationUseManualSetup ? (
+            <IntegrationSetup
+              integrationSlug={integrationSlug}
+              project={project}
+              onClickManualSetup={() => {
+                setIntegrationUseManualSetup(true);
+              }}
+            />
+          ) : (
+            <ProjecDocs
+              platform={loadedPlatform}
+              organization={organization}
+              project={project}
+              hasError={hasError}
+              platformDocs={platformDocs}
+              onRetry={fetchData}
+            />
+          )}
         </MainContent>
       </Wrapper>