Browse Source

feat(replay): add mobile platforms to onboarding sidebar (#76709)

closes https://github.com/getsentry/sentry/issues/67695

### android:


https://github.com/user-attachments/assets/4ba7d3cc-b4a7-4678-8fe6-5c55baed873c


### react native:


https://github.com/user-attachments/assets/bb614c5d-6b55-4a97-b1db-a38e564cf0c1


### apple:


https://github.com/user-attachments/assets/6e30f742-13e2-498f-9d2b-261ad3c9dc18
Michelle Zhang 6 months ago
parent
commit
448060a476

+ 1 - 0
static/app/components/feedback/feedbackOnboarding/sidebar.tsx

@@ -334,6 +334,7 @@ function OnboardingContent({currentProject}: {currentProject: Project}) {
     ) {
       return 'feedbackOnboardingNpm';
     }
+    // TODO: update this when we add feedback to the loader
     return 'replayOnboardingJsLoader';
   }
 

+ 2 - 2
static/app/components/onboarding/gettingStartedDoc/types.ts

@@ -94,8 +94,8 @@ export interface Docs<PlatformOptions extends BasePlatformOptions = BasePlatform
   feedbackOnboardingCrashApi?: OnboardingConfig<PlatformOptions>;
   feedbackOnboardingNpm?: OnboardingConfig<PlatformOptions>;
   platformOptions?: PlatformOptions;
+  replayOnboarding?: OnboardingConfig<PlatformOptions>;
   replayOnboardingJsLoader?: OnboardingConfig<PlatformOptions>;
-  replayOnboardingNpm?: OnboardingConfig<PlatformOptions>;
 }
 
 export type ConfigType =
@@ -103,6 +103,6 @@ export type ConfigType =
   | 'feedbackOnboardingNpm'
   | 'feedbackOnboardingCrashApi'
   | 'crashReportOnboarding'
-  | 'replayOnboardingNpm'
+  | 'replayOnboarding'
   | 'replayOnboardingJsLoader'
   | 'customMetricsOnboarding';

+ 1 - 1
static/app/components/onboarding/gettingStartedDoc/utils/index.tsx

@@ -75,7 +75,7 @@ export function MobileBetaBanner({link}: {link: string}) {
   return (
     <Alert type="info" showIcon>
       {tct(
-        `Currently, Mobile Replay is in beta. You can [link:read our docs] to learn how to set it up for your project.`,
+        `Currently, Mobile Replay is in beta. To learn more, you can [link:read our docs].`,
         {
           link: <ExternalLink href={link} />,
         }

+ 8 - 0
static/app/components/onboarding/gettingStartedDoc/utils/replayOnboarding.tsx

@@ -2,6 +2,14 @@ import ExternalLink from 'sentry/components/links/externalLink';
 import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types';
 import {tct} from 'sentry/locale';
 
+export const getReplayMobileConfigureDescription = ({link}: {link: string}) =>
+  tct(
+    'The SDK aggressively redacts all text and images. We plan to add fine controls for redacting, but currently, we just allow either on or off. Learn more about configuring Session Replay by reading the [link:configuration docs].',
+    {
+      link: <ExternalLink href={link} />,
+    }
+  );
+
 export const getReplayConfigureDescription = ({link}: {link: string}) =>
   tct(
     'Add the following to your SDK config. There are several privacy and sampling options available, all of which can be set using the [code:integrations] constructor. Learn more about configuring Session Replay by reading the [link:configuration docs].',

+ 12 - 3
static/app/components/replaysOnboarding/replayOnboardingLayout.tsx

@@ -8,6 +8,7 @@ import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/ty
 import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries';
 import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl';
 import ReplayConfigToggle from 'sentry/components/replaysOnboarding/replayConfigToggle';
+import {space} from 'sentry/styles/space';
 import useOrganization from 'sentry/utils/useOrganization';
 
 export function ReplayOnboardingLayout({
@@ -18,14 +19,15 @@ export function ReplayOnboardingLayout({
   projectSlug,
   newOrg,
   configType = 'onboarding',
-}: OnboardingLayoutProps) {
+  hideMaskBlockToggles,
+}: OnboardingLayoutProps & {hideMaskBlockToggles?: boolean}) {
   const organization = useOrganization();
   const {isPending: isLoadingRegistry, data: registryData} =
     useSourcePackageRegistries(organization);
   const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions);
   const [mask, setMask] = useState(true);
   const [block, setBlock] = useState(true);
-  const {steps} = useMemo(() => {
+  const {introduction, steps} = useMemo(() => {
     const doc = docsConfig[configType] ?? docsConfig.onboarding;
 
     const docParams: DocsParams<any> = {
@@ -78,6 +80,7 @@ export function ReplayOnboardingLayout({
   return (
     <AuthTokenGeneratorProvider projectSlug={projectSlug}>
       <Wrapper>
+        {introduction && <Introduction>{introduction}</Introduction>}
         <Steps>
           {steps.map(step =>
             step.type === StepType.CONFIGURE ? (
@@ -85,7 +88,7 @@ export function ReplayOnboardingLayout({
                 key={step.title ?? step.type}
                 {...{
                   ...step,
-                  codeHeader: (
+                  codeHeader: hideMaskBlockToggles ? null : (
                     <ReplayConfigToggle
                       blockToggle={block}
                       maskToggle={mask}
@@ -124,3 +127,9 @@ const Wrapper = styled('div')`
     }
   }
 `;
+
+const Introduction = styled('div')`
+  display: flex;
+  flex-direction: column;
+  margin: 0 0 ${space(2)} 0;
+`;

+ 23 - 42
static/app/components/replaysOnboarding/sidebar.tsx

@@ -10,7 +10,6 @@ import {CompactSelect} from 'sentry/components/compactSelect';
 import RadioGroup from 'sentry/components/forms/controls/radioGroup';
 import IdBadge from 'sentry/components/idBadge';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
-import {MobileBetaBanner} from 'sentry/components/onboarding/gettingStartedDoc/utils';
 import useCurrentProjectState from 'sentry/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState';
 import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted';
 import {PlatformOptionDropdown} from 'sentry/components/replaysOnboarding/platformOptionDropdown';
@@ -21,10 +20,10 @@ import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
 import {SidebarPanelKey} from 'sentry/components/sidebar/types';
 import TextOverflow from 'sentry/components/textOverflow';
 import {
-  backend,
   replayBackendPlatforms,
   replayFrontendPlatforms,
   replayJsLoaderInstructionsPlatformList,
+  replayMobilePlatforms,
   replayOnboardingPlatforms,
   replayPlatforms,
 } from 'sentry/data/platformCategories';
@@ -162,6 +161,8 @@ function OnboardingContent({
   currentProject: Project;
   hasDocs: boolean;
 }) {
+  const organization = useOrganization();
+
   const jsFrameworkSelectOptions = replayJsFrameworkOptions().map(platform => {
     return {
       value: platform.id,
@@ -175,40 +176,29 @@ function OnboardingContent({
     };
   });
 
-  const organization = useOrganization();
   const [jsFramework, setJsFramework] = useState<{
     value: PlatformKey;
     label?: ReactNode;
     textValue?: string;
   }>(jsFrameworkSelectOptions[0]);
 
-  const defaultTab =
-    currentProject.platform && backend.includes(currentProject.platform)
-      ? 'jsLoader'
-      : 'npm';
-
-  const {getParamValue: setupMode, setParamValue: setSetupMode} = useUrlParams(
-    'mode',
-    defaultTab
-  );
-
-  const showJsFrameworkInstructions =
-    currentProject.platform &&
-    replayBackendPlatforms.includes(currentProject.platform) &&
-    setupMode() === 'npm';
-
+  const backendPlatform =
+    currentProject.platform && replayBackendPlatforms.includes(currentProject.platform);
+  const mobilePlatform =
+    currentProject.platform && replayMobilePlatforms.includes(currentProject.platform);
   const npmOnlyFramework =
     currentProject.platform &&
     replayFrontendPlatforms
       .filter((p): p is PlatformKey => p !== 'javascript')
       .includes(currentProject.platform);
 
-  const showRadioButtons =
-    currentProject.platform &&
-    replayJsLoaderInstructionsPlatformList.includes(currentProject.platform);
+  const defaultTab = backendPlatform ? 'jsLoader' : 'npm';
+  const {getParamValue: setupMode, setParamValue: setSetupMode} = useUrlParams(
+    'mode',
+    defaultTab
+  );
 
-  const backendPlatforms =
-    currentProject.platform && replayBackendPlatforms.includes(currentProject.platform);
+  const showJsFrameworkInstructions = backendPlatform && setupMode() === 'npm';
 
   const currentPlatform = currentProject.platform
     ? platforms.find(p => p.id === currentProject.platform) ?? otherPlatform
@@ -240,6 +230,10 @@ function OnboardingContent({
     productType: 'replay',
   });
 
+  const showRadioButtons =
+    currentProject.platform &&
+    replayJsLoaderInstructionsPlatformList.includes(currentProject.platform);
+
   const radioButtons = (
     <Header>
       {showRadioButtons ? (
@@ -248,7 +242,7 @@ function OnboardingContent({
           choices={[
             [
               'npm',
-              backendPlatforms ? (
+              backendPlatform ? (
                 <PlatformSelect key="platform-select">
                   {tct('I use [platformSelect]', {
                     platformSelect: (
@@ -283,6 +277,7 @@ function OnboardingContent({
           onChange={setSetupMode}
         />
       ) : (
+        !mobilePlatform &&
         docs?.platformOptions &&
         !isProjKeysLoading && (
           <PlatformSelect>
@@ -306,22 +301,6 @@ function OnboardingContent({
     );
   }
 
-  // TODO: remove once we have mobile replay onboarding
-  if (['android', 'react-native'].includes(currentPlatform.language)) {
-    return (
-      <MobileBetaBanner
-        link={`https://docs.sentry.io/platforms/${currentPlatform.language}/session-replay/`}
-      />
-    );
-  }
-  if (currentPlatform.language === 'apple') {
-    return (
-      <MobileBetaBanner
-        link={`https://docs.sentry.io/platforms/apple/guides/ios/session-replay/`}
-      />
-    );
-  }
-
   const doesNotSupportReplay = currentProject.platform
     ? !replayPlatforms.includes(currentProject.platform)
     : true;
@@ -375,6 +354,7 @@ function OnboardingContent({
     <Fragment>
       {radioButtons}
       <ReplayOnboardingLayout
+        hideMaskBlockToggles={mobilePlatform}
         docsConfig={docs}
         dsn={dsn}
         activeProductSelection={[]}
@@ -384,8 +364,9 @@ function OnboardingContent({
         configType={
           setupMode() === 'npm' || // switched to NPM option
           (!setupMode() && defaultTab === 'npm') || // default value for FE frameworks when ?mode={...} in URL is not set yet
-          npmOnlyFramework // even if '?mode=jsLoader', only show npm instructions for FE frameworks
-            ? 'replayOnboardingNpm'
+          npmOnlyFramework ||
+          mobilePlatform // even if '?mode=jsLoader', only show npm/default instructions for FE frameworks & mobile platforms
+            ? 'replayOnboarding'
             : 'replayOnboardingJsLoader'
         }
       />

+ 1 - 0
static/app/data/platformCategories.tsx

@@ -475,6 +475,7 @@ export const replayPlatforms: readonly PlatformKey[] = [
 export const replayOnboardingPlatforms: readonly PlatformKey[] = [
   ...replayFrontendPlatforms.filter(p => !['javascript-backbone'].includes(p)),
   ...replayBackendPlatforms,
+  ...replayMobilePlatforms,
 ];
 
 // These are the supported replay platforms that can also be set up using the JS loader.

+ 153 - 0
static/app/gettingStartedDocs/android/android.tsx

@@ -10,7 +10,9 @@ import type {
   DocsParams,
   OnboardingConfig,
 } from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {MobileBetaBanner} from 'sentry/components/onboarding/gettingStartedDoc/utils';
 import {getAndroidMetricsOnboarding} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding';
+import {getReplayMobileConfigureDescription} from 'sentry/components/onboarding/gettingStartedDoc/utils/replayOnboarding';
 import {feedbackOnboardingCrashApiJava} from 'sentry/gettingStartedDocs/java/java';
 import {t, tct} from 'sentry/locale';
 import {getPackageVersion} from 'sentry/utils/gettingStartedDocs/getPackageVersion';
@@ -93,6 +95,24 @@ val breakWorld = Button(this).apply {
 addContentView(breakWorld, ViewGroup.LayoutParams(
   ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))`;
 
+const getReplaySetupSnippetKotlin = (params: Params) => `
+SentryAndroid.init(context) { options ->
+  options.dsn = "${params.dsn.public}"
+  options.isDebug = true
+
+  // Currently under experimental options:
+  options.experimental.sessionReplay.errorSampleRate = 1.0
+  options.experimental.sessionReplay.sessionSampleRate = 1.0
+}`;
+
+const getReplaySetupSnippetXml = () => `
+<meta-data android:name="io.sentry.session-replay.error-sample-rate" android:value="1.0" />
+<meta-data android:name="io.sentry.session-replay.session-sample-rate" android:value="1.0" />`;
+
+const getReplayConfigurationSnippet = () => `
+options.experimental.sessionReplay.redactAllText = true
+options.experimental.sessionReplay.redactAllImages = true`;
+
 const onboarding: OnboardingConfig<PlatformOptions> = {
   install: params =>
     isAutoInstall(params)
@@ -283,12 +303,145 @@ const onboarding: OnboardingConfig<PlatformOptions> = {
         ],
 };
 
+const replayOnboarding: OnboardingConfig<PlatformOptions> = {
+  introduction: () => (
+    <MobileBetaBanner link="https://docs.sentry.io/platforms/android/session-replay/" />
+  ),
+  install: (params: Params) => [
+    {
+      type: StepType.INSTALL,
+      description: tct(
+        "Make sure your Sentry Android SDK version is at least 7.12.0. The easiest way to update through the Sentry Android Gradle plugin to your app module's [code:build.gradle] file.",
+        {code: <code />}
+      ),
+      configurations: [
+        {
+          code: [
+            {
+              label: 'Groovy',
+              value: 'groovy',
+              language: 'groovy',
+              filename: 'app/build.gradle',
+              code: `plugins {
+  id "com.android.application"
+  id "io.sentry.android.gradle" version "${getPackageVersion(
+    params,
+    'sentry.java.android.gradle-plugin',
+    '4.11.0'
+  )}"
+}`,
+            },
+            {
+              label: 'Kotlin',
+              value: 'kotlin',
+              language: 'kotlin',
+              filename: 'app/build.gradle.kts',
+              code: `plugins {
+  id("com.android.application")
+  id("io.sentry.android.gradle") version "${getPackageVersion(
+    params,
+    'sentry.java.android.gradle-plugin',
+    '4.11.0'
+  )}"
+}`,
+            },
+          ],
+        },
+        {
+          description: tct(
+            'If you have the SDK installed without the Sentry Gradle Plugin, you can update the version directly in the [code:build.gradle] through:',
+            {code: <code />}
+          ),
+        },
+        {
+          code: [
+            {
+              label: 'Groovy',
+              value: 'groovy',
+              language: 'groovy',
+              filename: 'app/build.gradle',
+              code: `dependencies {
+    implementation 'io.sentry:sentry-android:${getPackageVersion(
+      params,
+      'sentry.java.android',
+      '7.14.0'
+    )}'
+}`,
+            },
+            {
+              label: 'Kotlin',
+              value: 'kotlin',
+              language: 'kotlin',
+              filename: 'app/build.gradle.kts',
+              code: `dependencies {
+    implementation("io.sentry:sentry-android:${getPackageVersion(
+      params,
+      'sentry.java.android',
+      '7.14.0'
+    )}")
+}`,
+            },
+          ],
+        },
+        {
+          description: t(
+            'To set up the integration, add the following to your Sentry initialization:'
+          ),
+        },
+        {
+          code: [
+            {
+              label: 'Kotlin',
+              value: 'kotlin',
+              language: 'kotlin',
+              code: getReplaySetupSnippetKotlin(params),
+            },
+            {
+              label: 'XML',
+              value: 'xml',
+              language: 'xml',
+              filename: 'AndroidManifest.xml',
+              code: getReplaySetupSnippetXml(),
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  configure: () => [
+    {
+      type: StepType.CONFIGURE,
+      description: getReplayMobileConfigureDescription({
+        link: 'https://docs.sentry.io/platforms/android/session-replay/#privacy',
+      }),
+      configurations: [
+        {
+          description: t(
+            'The following code is the default configuration, which masks and blocks everything.'
+          ),
+          code: [
+            {
+              label: 'Kotlin',
+              value: 'kotlin',
+              language: 'kotlin',
+              code: getReplayConfigurationSnippet(),
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  verify: () => [],
+  nextSteps: () => [],
+};
+
 const docs: Docs<PlatformOptions> = {
   onboarding,
   feedbackOnboardingCrashApi: feedbackOnboardingCrashApiJava,
   crashReportOnboarding: feedbackOnboardingCrashApiJava,
   customMetricsOnboarding: getAndroidMetricsOnboarding(),
   platformOptions,
+  replayOnboarding,
 };
 
 export default docs;

+ 103 - 0
static/app/gettingStartedDocs/apple/ios.tsx

@@ -9,7 +9,9 @@ import type {
   DocsParams,
   OnboardingConfig,
 } from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {MobileBetaBanner} from 'sentry/components/onboarding/gettingStartedDoc/utils';
 import {metricTagsExplanation} from 'sentry/components/onboarding/gettingStartedDoc/utils/metricsOnboarding';
+import {getReplayMobileConfigureDescription} from 'sentry/components/onboarding/gettingStartedDoc/utils/replayOnboarding';
 import {appleFeedbackOnboarding} from 'sentry/gettingStartedDocs/apple/macos';
 import {t, tct} from 'sentry/locale';
 import {getPackageVersion} from 'sentry/utils/gettingStartedDocs/getPackageVersion';
@@ -244,6 +246,20 @@ const getVerifyMetricsSnippetObjC = () => `
   tags: @{ @"screen" : @"login" }
 ];`;
 
+const getReplaySetupSnippet = (params: Params) => `
+SentrySDK.start(configureOptions: { options in
+  options.dsn = "${params.dsn.public}"
+  options.debug = true
+
+  // Currently under experimental options:
+  options.experimental.sessionReplay.onErrorSampleRate = 1.0
+  options.experimental.sessionReplay.sessionSampleRate = 1.0
+})`;
+
+const getReplayConfigurationSnippet = () => `
+options.experimental.sessionReplay.redactAllText = true
+options.experimental.sessionReplay.redactAllImages = true`;
+
 const onboarding: OnboardingConfig<PlatformOptions> = {
   install: params =>
     isAutoInstall(params)
@@ -615,12 +631,99 @@ const metricsOnboarding: OnboardingConfig<PlatformOptions> = {
   ],
 };
 
+const replayOnboarding: OnboardingConfig<PlatformOptions> = {
+  introduction: () => (
+    <MobileBetaBanner link="https://docs.sentry.io/platforms/android/session-replay/" />
+  ),
+  install: (params: Params) => [
+    {
+      type: StepType.INSTALL,
+      description: t(
+        'Make sure your Sentry Cocoa SDK version is at least 8.31.1. If you already have the SDK installed, you can update it to the latest version with:'
+      ),
+      configurations: [
+        {
+          code: [
+            {
+              label: 'SPM',
+              value: 'spm',
+              language: 'swift',
+              code: `.package(url: "https://github.com/getsentry/sentry-cocoa", from: "${getPackageVersion(
+                params,
+                'sentry.cocoa',
+                '8.36.0'
+              )}"),`,
+            },
+            {
+              label: 'CocoaPods',
+              value: 'cocoapods',
+              language: 'ruby',
+              code: `pod update`,
+            },
+            {
+              label: 'Carthage',
+              value: 'carthage',
+              language: 'swift',
+              code: `github "getsentry/sentry-cocoa" "${getPackageVersion(
+                params,
+                'sentry.cocoa',
+                '8.36.0'
+              )}"`,
+            },
+          ],
+        },
+        {
+          description: t(
+            'To set up the integration, add the following to your Sentry initialization:'
+          ),
+        },
+        {
+          code: [
+            {
+              label: 'Swift',
+              value: 'swift',
+              language: 'swift',
+              code: getReplaySetupSnippet(params),
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  configure: () => [
+    {
+      type: StepType.CONFIGURE,
+      description: getReplayMobileConfigureDescription({
+        link: 'https://docs.sentry.io/platforms/apple/guides/ios/session-replay/#privacy',
+      }),
+      configurations: [
+        {
+          description: t(
+            'The following code is the default configuration, which masks and blocks everything.'
+          ),
+          code: [
+            {
+              label: 'Swift',
+              value: 'swift',
+              language: 'swift',
+              code: getReplayConfigurationSnippet(),
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  verify: () => [],
+  nextSteps: () => [],
+};
+
 const docs: Docs<PlatformOptions> = {
   onboarding,
   feedbackOnboardingCrashApi: appleFeedbackOnboarding,
   crashReportOnboarding: appleFeedbackOnboarding,
   customMetricsOnboarding: metricsOnboarding,
   platformOptions,
+  replayOnboarding,
 };
 
 export default docs;

+ 1 - 1
static/app/gettingStartedDocs/capacitor/capacitor.tsx

@@ -487,7 +487,7 @@ const docs: Docs<PlatformOptions> = {
   onboarding,
   platformOptions,
   feedbackOnboardingNpm: feedbackOnboarding,
-  replayOnboardingNpm: replayOnboarding,
+  replayOnboarding,
   crashReportOnboarding,
 };
 

Some files were not shown because too many files changed in this diff