sourceMapDebug.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import uniqBy from 'lodash/uniqBy';
  4. import Alert from 'sentry/components/alert';
  5. import {Button} from 'sentry/components/button';
  6. import SourceMapsWizard from 'sentry/components/events/interfaces/crashContent/exception/sourcemapsWizard';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import List from 'sentry/components/list';
  9. import ListItem from 'sentry/components/list/listItem';
  10. import {IconWarning} from 'sentry/icons';
  11. import {t, tct, tn} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import {Event} from 'sentry/types';
  14. import {defined} from 'sentry/utils';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import {getAnalyticsDataForEvent} from 'sentry/utils/events';
  17. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import {
  20. SourceMapDebugError,
  21. SourceMapDebugResponse,
  22. SourceMapProcessingIssueType,
  23. StacktraceFilenameQuery,
  24. useSourceMapDebugQueries,
  25. } from './useSourceMapDebug';
  26. import {sourceMapSdkDocsMap} from './utils';
  27. const shortPathPlatforms = ['javascript', 'node', 'react-native'];
  28. const sentryInit = <code>Sentry.init</code>;
  29. function getErrorMessage(
  30. error: SourceMapDebugError,
  31. sdkName?: string
  32. ): Array<{
  33. title: string;
  34. /**
  35. * Expandable description
  36. */
  37. desc?: React.ReactNode;
  38. docsLink?: string;
  39. }> {
  40. const docPlatform = (sdkName && sourceMapSdkDocsMap[sdkName]) ?? 'javascript';
  41. const useShortPath = shortPathPlatforms.includes(docPlatform);
  42. const baseSourceMapDocsLink = useShortPath
  43. ? `https://docs.sentry.io/platforms/${docPlatform}/sourcemaps/`
  44. : `https://docs.sentry.io/platforms/javascript/guides/${docPlatform}/sourcemaps/`;
  45. function getTroubleshootingLink(section?: string) {
  46. // react-native has a different troubleshooting page
  47. if (docPlatform === 'react-native') {
  48. return 'https://docs.sentry.io/platforms/react-native/troubleshooting/#source-maps';
  49. }
  50. return (
  51. `${baseSourceMapDocsLink}troubleshooting_js/legacy-uploading-methods/` +
  52. (section ? `#${section}` : '')
  53. );
  54. }
  55. const defaultDocsLink = `${baseSourceMapDocsLink}#uploading-source-maps`;
  56. switch (error.type) {
  57. case SourceMapProcessingIssueType.MISSING_RELEASE:
  58. return [
  59. {
  60. title: t('Event missing Release tag'),
  61. desc: t(
  62. 'Integrate Sentry into your release pipeline using a tool like Webpack or the CLI.'
  63. ),
  64. docsLink: defaultDocsLink,
  65. },
  66. ];
  67. case SourceMapProcessingIssueType.PARTIAL_MATCH:
  68. return [
  69. {
  70. title: t('Partial Absolute Path Match'),
  71. desc: tct(
  72. 'The abs_path of the stack frame is a partial match. The stack frame has the path [absPath] which is a partial match to [partialMatchPath]. You need to update the value for the URL prefix argument or `includes` in your config options to include [urlPrefix]',
  73. {
  74. absPath: <code>{error.data.absPath}</code>,
  75. partialMatchPath: <code>{error.data.partialMatchPath}</code>,
  76. urlPrefix: <code>{error.data.urlPrefix}</code>,
  77. }
  78. ),
  79. docsLink: getTroubleshootingLink(
  80. 'verify-artifact-names-match-stack-trace-frames'
  81. ),
  82. },
  83. ];
  84. case SourceMapProcessingIssueType.MISSING_SOURCEMAPS:
  85. return [
  86. {
  87. title: t('Source Maps not uploaded'),
  88. desc: t(
  89. "It looks like you're creating, but not uploading your source maps. Read our docs for troubleshooting help."
  90. ),
  91. docsLink: defaultDocsLink,
  92. },
  93. ];
  94. case SourceMapProcessingIssueType.URL_NOT_VALID:
  95. return [
  96. {
  97. title: t('Invalid Absolute Path URL'),
  98. desc: tct(
  99. 'The [literalAbsPath] of the stack frame is [absPath] which is not a valid URL. Read our docs for troubleshooting help.',
  100. {
  101. absPath: <code>{error.data.absPath}</code>,
  102. literalAbsPath: <code>abs_path</code>,
  103. }
  104. ),
  105. docsLink: getTroubleshootingLink(
  106. 'verify-artifact-names-match-stack-trace-frames'
  107. ),
  108. },
  109. ];
  110. case SourceMapProcessingIssueType.NO_URL_MATCH:
  111. return [
  112. {
  113. title: t('Absolute Path Mismatch'),
  114. desc: tct(
  115. "The given [literalAbsPath] of the stack frame is [absPath] which doesn't match any release artifact. Read our docs for troubleshooting help.",
  116. {
  117. absPath: <code>{error.data.absPath}</code>,
  118. literalAbsPath: <code>abs_path</code>,
  119. }
  120. ),
  121. docsLink: getTroubleshootingLink(
  122. 'verify-artifact-names-match-stack-trace-frames'
  123. ),
  124. },
  125. ];
  126. case SourceMapProcessingIssueType.DIST_MISMATCH:
  127. return [
  128. {
  129. title: t('Dist Mismatch'),
  130. desc: tct(
  131. "The distribution identifier you're providing doesn't match. The [literalDist] value of [dist] configured in your [init] must be the same as the one used during source map upload. Read our docs for troubleshooting help.",
  132. {
  133. init: sentryInit,
  134. dist: <code>dist</code>,
  135. literalDist: <code>dist</code>,
  136. }
  137. ),
  138. docsLink: getTroubleshootingLink(
  139. 'verify-artifact-distribution-value-matches-value-configured-in-your-sdk'
  140. ),
  141. },
  142. ];
  143. case SourceMapProcessingIssueType.SOURCEMAP_NOT_FOUND:
  144. return [
  145. {
  146. title: t("Source Map File doesn't exist"),
  147. desc: t(
  148. "Sentry couldn't fetch the source map file for this event. Read our docs for troubleshooting help."
  149. ),
  150. docsLink: getTroubleshootingLink(),
  151. },
  152. ];
  153. // Need to return something but this does not need to follow the pattern since it uses a different alert
  154. case SourceMapProcessingIssueType.DEBUG_ID_NO_SOURCEMAPS:
  155. return [{title: 'Debug Id but no Sourcemaps'}];
  156. case SourceMapProcessingIssueType.UNKNOWN_ERROR:
  157. default:
  158. return [];
  159. }
  160. }
  161. interface ExpandableErrorListProps {
  162. title: React.ReactNode;
  163. children?: React.ReactNode;
  164. docsLink?: React.ReactNode;
  165. onExpandClick?: () => void;
  166. }
  167. /**
  168. * Kinda making this reuseable since we have this pattern in a few places
  169. */
  170. function ExpandableErrorList({
  171. title,
  172. children,
  173. docsLink,
  174. onExpandClick,
  175. }: ExpandableErrorListProps) {
  176. const [expanded, setExpanded] = useState(false);
  177. return (
  178. <List symbol="bullet">
  179. <StyledListItem>
  180. <ErrorTitleFlex>
  181. <ErrorTitleFlex>
  182. <strong>{title}</strong>
  183. {children && (
  184. <ToggleButton
  185. priority="link"
  186. size="zero"
  187. onClick={() => {
  188. setExpanded(!expanded);
  189. onExpandClick?.();
  190. }}
  191. >
  192. {expanded ? t('Collapse') : t('Expand')}
  193. </ToggleButton>
  194. )}
  195. </ErrorTitleFlex>
  196. {docsLink}
  197. </ErrorTitleFlex>
  198. {expanded && <div>{children}</div>}
  199. </StyledListItem>
  200. </List>
  201. );
  202. }
  203. function combineErrors(
  204. response: Array<SourceMapDebugResponse | undefined | null>,
  205. sdkName?: string
  206. ) {
  207. const combinedErrors = uniqBy(
  208. response
  209. .map(res => res?.errors)
  210. .flat()
  211. .filter(defined),
  212. error => error?.type
  213. );
  214. const errors = combinedErrors
  215. .map(error =>
  216. getErrorMessage(error, sdkName).map(message => ({...message, type: error.type}))
  217. )
  218. .flat();
  219. return errors;
  220. }
  221. interface SourcemapDebugProps {
  222. /**
  223. * A subset of the total error frames to validate sourcemaps
  224. */
  225. debugFrames: StacktraceFilenameQuery[];
  226. event: Event;
  227. }
  228. export function SourceMapDebug({debugFrames, event}: SourcemapDebugProps) {
  229. const sdkName = event.sdk?.name;
  230. const organization = useOrganization();
  231. const results = useSourceMapDebugQueries(debugFrames.map(debug => debug.query));
  232. const isLoading = results.every(result => result.isLoading);
  233. const errorMessages = combineErrors(
  234. results.map(result => result.data).filter(defined),
  235. sdkName
  236. );
  237. useRouteAnalyticsParams({
  238. show_fix_source_map_cta: errorMessages.length > 0,
  239. source_map_debug_errors: errorMessages.map(error => error.type).join(','),
  240. });
  241. if (isLoading || !errorMessages.length) {
  242. return null;
  243. }
  244. const analyticsParams = {
  245. organization,
  246. project_id: event.projectID,
  247. group_id: event.groupID,
  248. ...getAnalyticsDataForEvent(event),
  249. };
  250. const handleDocsClick = (type: SourceMapProcessingIssueType) => {
  251. trackAnalytics('source_map_debug.docs_link_clicked', {
  252. ...analyticsParams,
  253. type,
  254. });
  255. };
  256. const handleExpandClick = (type: SourceMapProcessingIssueType) => {
  257. trackAnalytics('source_map_debug.expand_clicked', {
  258. ...analyticsParams,
  259. type,
  260. });
  261. };
  262. if (
  263. errorMessages.filter(
  264. error => error.type === SourceMapProcessingIssueType.DEBUG_ID_NO_SOURCEMAPS
  265. ).length > 0
  266. ) {
  267. return <SourceMapsWizard analyticsParams={analyticsParams} />;
  268. }
  269. return (
  270. <Alert
  271. defaultExpanded
  272. showIcon
  273. type="error"
  274. icon={<IconWarning />}
  275. expand={
  276. <Fragment>
  277. {errorMessages.map((message, idx) => {
  278. return (
  279. <ExpandableErrorList
  280. key={idx}
  281. title={message.title}
  282. docsLink={
  283. message.docsLink ? (
  284. <DocsExternalLink
  285. href={message.docsLink}
  286. onClick={() => handleDocsClick(message.type)}
  287. >
  288. {t('Read Guide')}
  289. </DocsExternalLink>
  290. ) : null
  291. }
  292. onExpandClick={() => handleExpandClick(message.type)}
  293. >
  294. {message.desc}
  295. </ExpandableErrorList>
  296. );
  297. })}
  298. </Fragment>
  299. }
  300. >
  301. {tn(
  302. "We've encountered %s problem un-minifying your applications source code!",
  303. "We've encountered %s problems un-minifying your applications source code!",
  304. errorMessages.length
  305. )}
  306. </Alert>
  307. );
  308. }
  309. const StyledListItem = styled(ListItem)`
  310. margin-bottom: ${space(0.75)};
  311. `;
  312. const ToggleButton = styled(Button)`
  313. color: ${p => p.theme.subText};
  314. :hover,
  315. :focus {
  316. color: ${p => p.theme.textColor};
  317. }
  318. `;
  319. const ErrorTitleFlex = styled('div')`
  320. display: flex;
  321. justify-content: space-between;
  322. align-items: center;
  323. gap: ${space(1)};
  324. `;
  325. const DocsExternalLink = styled(ExternalLink)`
  326. white-space: nowrap;
  327. `;