bundledetails.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import Card from 'sentry/components/card';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import PanelTable from 'sentry/components/panels/panelTable';
  7. import {SegmentedControl} from 'sentry/components/segmentedControl';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {formatBytesBase2} from 'sentry/utils';
  11. import useRouter from 'sentry/utils/useRouter';
  12. import {bundleStats as stats} from 'sentry/views/bundleAnalyzer';
  13. import {CardSection} from 'sentry/views/performance/transactionSummary/transactionVitals/styles';
  14. enum BundleType {
  15. ASSETS = 'assets',
  16. IN_APP = 'in_app',
  17. PACKAGES = 'packages',
  18. OTHER = 'other',
  19. }
  20. const BUNDLE_TYPES = [
  21. {
  22. key: BundleType.ASSETS,
  23. label: t('Assets'),
  24. },
  25. {
  26. key: BundleType.IN_APP,
  27. label: t('In App'),
  28. },
  29. {
  30. key: BundleType.PACKAGES,
  31. label: t('Packages'),
  32. },
  33. // {
  34. // key: BundleType.OTHER,
  35. // label: t('Other'),
  36. // },
  37. ];
  38. const chunks = stats.chunks;
  39. const assets = stats.assets.sort((a, b) => b.size - a.size);
  40. const modules = stats.modules;
  41. const inAppModules = modules
  42. .filter(
  43. m =>
  44. m.moduleType.startsWith('javascript') &&
  45. m.name.startsWith('./app') &&
  46. !m.name.endsWith('namespace object')
  47. )
  48. .sort((a, b) => b.size - a.size);
  49. const packageModules = modules
  50. .filter(m => m.name.startsWith('../node_modules'))
  51. .sort((a, b) => b.size - a.size);
  52. export default function BundleDetails() {
  53. const router = useRouter();
  54. const bundleType = router.location.query.bundleType ?? BundleType.ASSETS;
  55. function handleBundleAnalysisSelection(type: BundleType) {
  56. router.replace({
  57. ...router.location,
  58. query: {
  59. ...router.location.query,
  60. bundleType: type,
  61. },
  62. });
  63. }
  64. function getPanelItems() {
  65. switch (bundleType) {
  66. case BundleType.ASSETS: {
  67. return assets.map(asset => (
  68. <Fragment key={`asset-${asset.name}`}>
  69. <div>{asset.name}</div>
  70. <div>{formatBytesBase2(asset.size)}</div>
  71. </Fragment>
  72. ));
  73. }
  74. case BundleType.IN_APP: {
  75. return inAppModules.map(module => (
  76. <Fragment
  77. key={`module-${module.name}-${
  78. module?.chunks[0] ? module?.chunks[0] : '_sentry_no_chunk'
  79. }`}
  80. >
  81. <ExternalLink
  82. href={`https://github.com/getsentry/sentry/blob/master/static/${module.name.replace(
  83. './',
  84. ''
  85. )}`}
  86. >
  87. {module.name}
  88. </ExternalLink>
  89. <div>{formatBytesBase2(module.size)}</div>
  90. </Fragment>
  91. ));
  92. }
  93. case BundleType.PACKAGES: {
  94. return packageModules.map(module => {
  95. const npmPackageName = getNpmPackageFromNodeModules(module.name);
  96. return (
  97. <Fragment
  98. key={`module-${module.name}-${
  99. module?.chunks[0] ? module?.chunks[0] : '_sentry_no_chunk'
  100. }`}
  101. >
  102. <ExternalLink href={`https://www.npmjs.com/package/${npmPackageName}`}>
  103. {module.name.replace('../node_modules/', '')}
  104. </ExternalLink>
  105. <div>{formatBytesBase2(module.size)}</div>
  106. </Fragment>
  107. );
  108. });
  109. }
  110. default: {
  111. throw new Error('Invalid bundle type');
  112. }
  113. }
  114. }
  115. const entrypoints = chunks.filter(c => c.entry);
  116. return (
  117. <Layout.Body>
  118. <Layout.Main fullWidth>
  119. <CardWrapper>
  120. {entrypoints.map(entrypoint => (
  121. <StyledCard key={entrypoint.id}>
  122. <CardSection>
  123. <div>{entrypoint.id}</div>
  124. <StatNumber>{formatBytesBase2(entrypoint.size)}</StatNumber>
  125. </CardSection>
  126. </StyledCard>
  127. ))}
  128. </CardWrapper>
  129. <SegmentedControlWrapper key="segmented-control">
  130. <SegmentedControl
  131. aria-label={t('Bundle Analysis Type')}
  132. size="sm"
  133. value={bundleType}
  134. onChange={key => handleBundleAnalysisSelection(key as BundleType)}
  135. >
  136. {BUNDLE_TYPES.map(({key, label}) => (
  137. <SegmentedControl.Item key={key} textValue={label}>
  138. {label}
  139. </SegmentedControl.Item>
  140. ))}
  141. </SegmentedControl>
  142. </SegmentedControlWrapper>
  143. <PanelTable headers={[t('Name'), t('Size')]}>{getPanelItems()}</PanelTable>
  144. </Layout.Main>
  145. </Layout.Body>
  146. );
  147. }
  148. function getNpmPackageFromNodeModules(name: string): string {
  149. const path = name.replace('../node_modules/', '');
  150. const pathComponents = path.split('/');
  151. for (let i = pathComponents.length - 1; i >= 0; i--) {
  152. if (pathComponents[i] === 'node_modules') {
  153. const maybePackageName = pathComponents[i + 1];
  154. if (maybePackageName.startsWith('@')) {
  155. return maybePackageName + pathComponents[i + 2];
  156. }
  157. return maybePackageName;
  158. }
  159. }
  160. if (pathComponents[0].startsWith('@')) {
  161. return pathComponents[0] + pathComponents[1];
  162. }
  163. return pathComponents[0];
  164. }
  165. const StyledCard = styled(Card)`
  166. margin-right: ${space(1)};
  167. `;
  168. const SegmentedControlWrapper = styled('div')`
  169. padding-bottom: ${space(2)};
  170. `;
  171. export const StatNumber = styled('div')`
  172. font-size: 32px;
  173. `;
  174. export const CardWrapper = styled('div')`
  175. display: flex;
  176. `;