Repository: getsentry/sentry
Pull request: ref(source-map-debugger-modal): Display different copies based on the release userAgent
This PR updates the code to show custom messages in the Sentry debugger modal based on the user agent in the release. If the user agent isn't available, a more general message will be shown, as it is in production. 

In the future, it would be helpful always to have the tool used to upload sourcemaps available - maybe this is something we can implement in the sentry-cli - but for now, we're using the user agent in the release to show a more specific message to the user.

**Before**
![image](https://github.com/user-attachments/assets/944aa704-2770-4431-b7dd-6f74e7ec32b3)


**After**
![Screenshot 2025-03-13 at 06 42 43](https://github.com/user-attachments/assets/96a15824-8ad7-4e06-8606-1ea25af7e181)



- closes https://github.com/getsentry/sentry/issues/85617

Problem statement:
Show tool-specific copies and links based on user agent
We can detect the user's tool through the user agent, if available in the release object of an event. Based on this information, we can display relevant links and content tailored to the tool used.

Hints from issue discussion:

Commit message: Merge branch 'priscila/ref/debugger-modal/display-copies-based-on-user-agent-of-available' of github.com:getsentry/sentry into priscila/ref/debugger-modal/display-copies-based-on-user-agent-of-available

Patch to review:
diff --git a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx
index c20d71112a2be6..cd48590e399847 100644
--- a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx
+++ b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx
@@ -156,6 +156,7 @@ export function prepareSourceMapDebuggerFrameInformation(
     releaseHasSomeArtifact: sourceMapDebuggerData.release_has_some_artifact,
     releaseSourceMapReference:
       debuggerFrame.release_process?.source_map_reference ?? null,
+    releaseUserAgent: event.release?.userAgent ?? null,
     sdkDebugIdSupport: sourceMapDebuggerData.sdk_debug_id_support,
     sourceFileReleaseNameFetchingResult:
       debuggerFrame.release_process?.source_file_lookup_result ?? 'unsuccessful',
diff --git a/static/app/components/events/interfaces/sourceMapsDebuggerModal.spec.tsx b/static/app/components/events/interfaces/sourceMapsDebuggerModal.spec.tsx
index efa6e24d3656f6..ad8d3cb1638bb7 100644
--- a/static/app/components/events/interfaces/sourceMapsDebuggerModal.spec.tsx
+++ b/static/app/components/events/interfaces/sourceMapsDebuggerModal.spec.tsx
@@ -29,6 +29,7 @@ const defaultSourceResolutionResults: FrameSourceMapDebuggerData = {
   releaseProgress: 0,
   releaseProgressPercent: 0,
   releaseSourceMapReference: null,
+  releaseUserAgent: null,
   scrapingProgress: 0,
   scrapingProgressPercent: 0,
   sdkDebugIdSupport: 'full',
diff --git a/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx b/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx
index 0201bbcad1190e..1589ed9af7a0ad 100644
--- a/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx
+++ b/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx
@@ -118,6 +118,7 @@ export interface FrameSourceMapDebuggerData {
   releaseProgress: number;
   releaseProgressPercent: number;
   releaseSourceMapReference: string | null;
+  releaseUserAgent: string | null;
   scrapingProgress: number;
   scrapingProgressPercent: number;
   sdkDebugIdSupport: 'full' | 'needs-upgrade' | 'not-supported' | 'unofficial-sdk';
@@ -236,6 +237,8 @@ function getSourceMapsDocLinks({
   };
 }
 
+type SourceMapsDocLinks = ReturnType<typeof getSourceMapsDocLinks>;
+
 export function SourceMapsDebuggerModal({
   Body,
   Header,
@@ -621,6 +624,159 @@ function InstalledSdkChecklistItem({
   );
 }
 
+function getToolUsedToUploadSourceMaps({
+  releaseUserAgent,
+}: Pick<FrameSourceMapDebuggerData, 'releaseUserAgent'>) {
+  const tools = [
+    'sentry-cli',
+    'vite-plugin',
+    'webpack-plugin',
+    'rollup-plugin',
+    'esbuild-plugin',
+  ];
+
+  return tools.reduce(
+    (acc, tool) => {
+      const key = tool.replace(/-([a-z])/g, (_match, name) => name.toUpperCase());
+      acc[key] = releaseUserAgent?.includes(tool) ?? false;
+      return acc;
+    },
+    {} as Record<string, boolean>
+  );
+}
+
+type ToolUsedToUploadSourceMaps = ReturnType<typeof getToolUsedToUploadSourceMaps>;
+
+const pluginConfig: Record<
+  keyof Omit<ToolUsedToUploadSourceMaps, 'sentryCli'>,
+  {
+    configFile: string;
+    link: string;
+    name: string;
+  }
+> = {
+  vitePlugin: {
+    name: 'Vite',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/vite/',
+    configFile: 'vite.config.(js|ts)',
+  },
+  webpackPlugin: {
+    name: 'Webpack',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/webpack/',
+    configFile: 'webpack.config.(js|ts)',
+  },
+  rollupPlugin: {
+    name: 'Rollup',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/rollup/',
+    configFile: 'rollup.config.(js|ts)',
+  },
+  esbuildPlugin: {
+    name: 'Esbuild',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/esbuild/',
+    configFile: 'esbuild.config.(js|ts)',
+  },
+};
+
+function SentryPluginMessage({
+  toolUsedToUploadSourceMaps,
+  sourceMapsDocLinks,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  const activePlugin = Object.keys(pluginConfig).find(
+    key => toolUsedToUploadSourceMaps[key as keyof ToolUsedToUploadSourceMaps]
+  );
+
+  const plugin = activePlugin
+    ? pluginConfig[activePlugin as keyof typeof pluginConfig]
+    : undefined;
+
+  if (plugin) {
+    return (
+      <p>
+        {tct(
+          'For your Sentry [pluginNameAndLink] Plugin, check your [configFile] and ensure the plugin is active when building your production app. You cannot do two separate builds—one for uploading to Sentry with the plugin active, and one for deploying without it. The plugin needs to be active for every build.',
+          {
+            pluginNameAndLink: (
+              <ExternalLinkWithIcon href={plugin.link}>
+                {plugin.name}
+              </ExternalLinkWithIcon>
+            ),
+            configFile: <code>{plugin.configFile}</code>,
+          }
+        )}
+      </p>
+    );
+  }
+
+  return (
+    <p>
+      {tct(
+        'If you are using a [bundlerPluginRepoLink:Sentry Plugin for your Bundler], the plugin needs to be active when building your production app. You cannot do two separate builds, for example, one for uploading to Sentry with the plugin being active and one for deploying without the plugin. The plugin needs to be active for every build.',
+        {
+          bundlerPluginRepoLink: (
+            <ExternalLinkWithIcon href={sourceMapsDocLinks.bundlerPluginRepoLink} />
+          ),
+        }
+      )}
+    </p>
+  );
+}
+
+function SentryCliMessage({
+  sourceResolutionResults,
+  sourceMapsDocLinks,
+  toolUsedToUploadSourceMaps,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks & {
+    sentryCli: NonNullable<SourceMapsDocLinks['sentryCli']>;
+  };
+  sourceResolutionResults: FrameSourceMapDebuggerData;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  if (isReactNativeSDK({sdkName: sourceResolutionResults.sdkName})) {
+    return (
+      <p>
+        {tct(
+          'When manually creating source maps, ensure that you upload the exact same files that were bundled into the application package. For details, see the [uploadingSourceMapsLink:Uploading Source Maps documentation]',
+          {
+            uploadingSourceMapsLink: (
+              <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />
+            ),
+          }
+        )}
+      </p>
+    );
+  }
+
+  if (toolUsedToUploadSourceMaps.sentryCli) {
+    return (
+      <p>
+        {tct(
+          'Since you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
+          {
+            sentryCliLink: <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />,
+            injectCommand: <MonoBlock>sentry-cli sourcemaps inject</MonoBlock>,
+          }
+        )}
+      </p>
+    );
+  }
+
+  return (
+    <p>
+      {tct(
+        'If you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
+        {
+          sentryCliLink: <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />,
+          injectCommand: <MonoBlock>sentry-cli sourcemaps inject</MonoBlock>,
+        }
+      )}
+    </p>
+  );
+}
+
 function HasDebugIdChecklistItem({
   sourceResolutionResults,
   shouldValidate,
@@ -657,6 +813,9 @@ function HasDebugIdChecklistItem({
   }
 
   if (sourceResolutionResults.uploadedSomeArtifactWithDebugId) {
+    const toolUsedToUploadSourceMaps = getToolUsedToUploadSourceMaps(
+      sourceResolutionResults
+    );
     return (
       <CheckListItem status="alert" title={errorMessage}>
         <CheckListInstruction type="muted">
@@ -693,41 +852,21 @@ function HasDebugIdChecklistItem({
                   "It seems you already uploaded artifacts with Debug IDs, however, this event doesn't contain any Debug IDs yet. Generally this means that you didn't deploy the same files you injected the Debug IDs into. For Sentry to be able to show your original source code, it is required that you deploy the exact same files that you uploaded to Sentry."
                 )}
               </p>
-              <p>
-                {tct(
-                  'If you are using a [bundlerPluginRepoLink:Sentry Plugin for your Bundler], the plugin needs to be active when building your production app. You cannot do two separate builds, for example, one for uploading to Sentry with the plugin being active and one for deploying without the plugin. The plugin needs to be active for every build.',
-                  {
-                    bundlerPluginRepoLink: (
-                      <ExternalLinkWithIcon
-                        href={sourceMapsDocLinks.bundlerPluginRepoLink}
-                      />
-                    ),
-                  }
-                )}
-              </p>
+              <SentryPluginMessage
+                toolUsedToUploadSourceMaps={toolUsedToUploadSourceMaps}
+                sourceMapsDocLinks={sourceMapsDocLinks}
+              />
             </Fragment>
           )}
           {defined(sourceMapsDocLinks.sentryCli) && (
-            <p>
-              {isReactNativeSDK({sdkName: sourceResolutionResults.sdkName})
-                ? tct(
-                    'When manually creating source maps, ensure that you upload the exact same files that were bundled into the application package. For details, see the [uploadingSourceMapsLink:Uploading Source Maps documentation]',
-                    {
-                      uploadingSourceMapsLink: (
-                        <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />
-                      ),
-                    }
-                  )
-                : tct(
-                    'If you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
-                    {
-                      sentryCliLink: (
-                        <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />
-                      ),
-                      injectCommand: <MonoBlock>sentry-cli sourcemaps inject</MonoBlock>,
-                    }
-                  )}
-            </p>
+            <SentryCliMessage
+              sourceMapsDocLinks={{
+                ...sourceMapsDocLinks,
+                sentryCli: sourceMapsDocLinks.sentryCli,
+              }}
+              sourceResolutionResults={sourceResolutionResults}
+              toolUsedToUploadSourceMaps={toolUsedToUploadSourceMaps}
+            />
           )}
           <p>
             {tct(


Code review comments:
diff_hunk: @@ -621,6 +624,159 @@ function InstalledSdkChecklistItem({
   );
 }
 
+function getToolUsedToUploadSourceMaps({
+  releaseUserAgent,
+}: Pick<FrameSourceMapDebuggerData, 'releaseUserAgent'>) {
+  const tools = [
+    'sentry-cli',
+    'vite-plugin',
+    'webpack-plugin',
+    'rollup-plugin',
+    'esbuild-plugin',
+  ];
+
+  return tools.reduce(
+    (acc, tool) => {
+      const key = tool.replace(/-([a-z])/g, (_match, name) => name.toUpperCase());
+      acc[key] = releaseUserAgent?.includes(tool) ?? false;
+      return acc;
+    },
+    {} as Record<string, boolean>
+  );
+}
+
+type ToolUsedToUploadSourceMaps = ReturnType<typeof getToolUsedToUploadSourceMaps>;
+
+const pluginConfig: Record<
+  keyof Omit<ToolUsedToUploadSourceMaps, 'sentryCli'>,
+  {
+    configFile: string;
+    link: string;
+    name: string;
+  }
+> = {
+  vitePlugin: {
+    name: 'Vite',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/vite/',
+    configFile: 'vite.config.(js|ts)',
+  },
+  webpackPlugin: {
+    name: 'Webpack',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/webpack/',
+    configFile: 'webpack.config.(js|ts)',
+  },
+  rollupPlugin: {
+    name: 'Rollup',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/rollup/',
+    configFile: 'rollup.config.(js|ts)',
+  },
+  esbuildPlugin: {
+    name: 'Esbuild',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/esbuild/',
+    configFile: 'esbuild.config.(js|ts)',
+  },
+};
+
+function SentryPluginMessage({
+  toolUsedToUploadSourceMaps,
+  sourceMapsDocLinks,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  const activePlugin = Object.keys(pluginConfig).find(
+    key => toolUsedToUploadSourceMaps[key as keyof ToolUsedToUploadSourceMaps]
+  );
+
+  const plugin = activePlugin
+    ? pluginConfig[activePlugin as keyof typeof pluginConfig]
+    : undefined;
+
+  if (plugin) {
+    return (
+      <p>
+        {tct(
+          'For your Sentry [pluginNameAndLink] Plugin, check your [configFile] and ensure the plugin is active when building your production app. You cannot do two separate builds—one for uploading to Sentry with the plugin active, and one for deploying without it. The plugin needs to be active for every build.',
+          {
+            pluginNameAndLink: (
+              <ExternalLinkWithIcon href={plugin.link}>
+                {plugin.name}
+              </ExternalLinkWithIcon>
+            ),
+            configFile: <code>{plugin.configFile}</code>,
+          }
+        )}
+      </p>
+    );
+  }
+
+  return (
+    <p>
+      {tct(
+        'If you are using a [bundlerPluginRepoLink:Sentry Plugin for your Bundler], the plugin needs to be active when building your production app. You cannot do two separate builds, for example, one for uploading to Sentry with the plugin being active and one for deploying without the plugin. The plugin needs to be active for every build.',
+        {
+          bundlerPluginRepoLink: (
+            <ExternalLinkWithIcon href={sourceMapsDocLinks.bundlerPluginRepoLink} />
+          ),
+        }
+      )}
+    </p>
+  );
+}
+
+function SentryCliMessage({
+  sourceResolutionResults,
+  sourceMapsDocLinks,
+  toolUsedToUploadSourceMaps,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks & {
+    sentryCli: NonNullable<SourceMapsDocLinks['sentryCli']>;
+  };
+  sourceResolutionResults: FrameSourceMapDebuggerData;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  if (isReactNativeSDK({sdkName: sourceResolutionResults.sdkName})) {
+    return (
+      <p>
+        {tct(
+          'When manually creating source maps, ensure that you upload the exact same files that were bundled into the application package. For details, see the [uploadingSourceMapsLink:Uploading Source Maps documentation]',
+          {
+            uploadingSourceMapsLink: (
+              <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />
+            ),
+          }
+        )}
+      </p>
+    );
+  }
+
+  if (toolUsedToUploadSourceMaps.sentryCli) {
+    return (
+      <p>
+        {tct(
+          'Since you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
+          {
+            sentryCliLink: <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />,
+            injectCommand: <MonoBlock>sentry-cli sourcemaps inject</MonoBlock>,
+          }
+        )}
+      </p>
+    );
+  }
+
+  return (
+    <p>
+      {tct(
+        'If you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
path: static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx
text: @user1:
```suggestion
        'If you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified.',
```

diff_hunk: @@ -621,6 +624,159 @@ function InstalledSdkChecklistItem({
   );
 }
 
+function getToolUsedToUploadSourceMaps({
+  releaseUserAgent,
+}: Pick<FrameSourceMapDebuggerData, 'releaseUserAgent'>) {
+  const tools = [
+    'sentry-cli',
+    'vite-plugin',
+    'webpack-plugin',
+    'rollup-plugin',
+    'esbuild-plugin',
+  ];
+
+  return tools.reduce(
+    (acc, tool) => {
+      const key = tool.replace(/-([a-z])/g, (_match, name) => name.toUpperCase());
+      acc[key] = releaseUserAgent?.includes(tool) ?? false;
+      return acc;
+    },
+    {} as Record<string, boolean>
+  );
+}
+
+type ToolUsedToUploadSourceMaps = ReturnType<typeof getToolUsedToUploadSourceMaps>;
+
+const pluginConfig: Record<
+  keyof Omit<ToolUsedToUploadSourceMaps, 'sentryCli'>,
+  {
+    configFile: string;
+    link: string;
+    name: string;
+  }
+> = {
+  vitePlugin: {
+    name: 'Vite',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/vite/',
+    configFile: 'vite.config.(js|ts)',
+  },
+  webpackPlugin: {
+    name: 'Webpack',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/webpack/',
+    configFile: 'webpack.config.(js|ts)',
+  },
+  rollupPlugin: {
+    name: 'Rollup',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/rollup/',
+    configFile: 'rollup.config.(js|ts)',
+  },
+  esbuildPlugin: {
+    name: 'Esbuild',
+    link: 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/esbuild/',
+    configFile: 'esbuild.config.(js|ts)',
+  },
+};
+
+function SentryPluginMessage({
+  toolUsedToUploadSourceMaps,
+  sourceMapsDocLinks,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  const activePlugin = Object.keys(pluginConfig).find(
+    key => toolUsedToUploadSourceMaps[key as keyof ToolUsedToUploadSourceMaps]
+  );
+
+  const plugin = activePlugin
+    ? pluginConfig[activePlugin as keyof typeof pluginConfig]
+    : undefined;
+
+  if (plugin) {
+    return (
+      <p>
+        {tct(
+          'For your Sentry [pluginNameAndLink] Plugin, check your [configFile] and ensure the plugin is active when building your production app. You cannot do two separate builds—one for uploading to Sentry with the plugin active, and one for deploying without it. The plugin needs to be active for every build.',
+          {
+            pluginNameAndLink: (
+              <ExternalLinkWithIcon href={plugin.link}>
+                {plugin.name}
+              </ExternalLinkWithIcon>
+            ),
+            configFile: <code>{plugin.configFile}</code>,
+          }
+        )}
+      </p>
+    );
+  }
+
+  return (
+    <p>
+      {tct(
+        'If you are using a [bundlerPluginRepoLink:Sentry Plugin for your Bundler], the plugin needs to be active when building your production app. You cannot do two separate builds, for example, one for uploading to Sentry with the plugin being active and one for deploying without the plugin. The plugin needs to be active for every build.',
+        {
+          bundlerPluginRepoLink: (
+            <ExternalLinkWithIcon href={sourceMapsDocLinks.bundlerPluginRepoLink} />
+          ),
+        }
+      )}
+    </p>
+  );
+}
+
+function SentryCliMessage({
+  sourceResolutionResults,
+  sourceMapsDocLinks,
+  toolUsedToUploadSourceMaps,
+}: {
+  sourceMapsDocLinks: SourceMapsDocLinks & {
+    sentryCli: NonNullable<SourceMapsDocLinks['sentryCli']>;
+  };
+  sourceResolutionResults: FrameSourceMapDebuggerData;
+  toolUsedToUploadSourceMaps: ToolUsedToUploadSourceMaps;
+}) {
+  if (isReactNativeSDK({sdkName: sourceResolutionResults.sdkName})) {
+    return (
+      <p>
+        {tct(
+          'When manually creating source maps, ensure that you upload the exact same files that were bundled into the application package. For details, see the [uploadingSourceMapsLink:Uploading Source Maps documentation]',
+          {
+            uploadingSourceMapsLink: (
+              <ExternalLinkWithIcon href={sourceMapsDocLinks.sentryCli} />
+            ),
+          }
+        )}
+      </p>
+    );
+  }
+
+  if (toolUsedToUploadSourceMaps.sentryCli) {
+    return (
+      <p>
+        {tct(
+          'Since you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified!',
path: static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx
text: @user1:
```suggestion
          'Since you are utilizing [sentryCliLink:Sentry CLI], ensure that you deploy the exact files that the [injectCommand] command has modified.',
```