Browse Source

feat(profiling): add export profile button (#37463)

* feat(profiling): add export profile button

* feat(profiling): add export profile button
Jonas 2 years ago
parent
commit
340be3b6a2

+ 61 - 45
static/app/components/profiling/FrameStack/frameStack.tsx

@@ -2,6 +2,7 @@ import {memo, MouseEventHandler, useCallback, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 
 import Button from 'sentry/components/button';
+import {ExportProfileButton} from 'sentry/components/profiling/exportProfileButton';
 import {IconPanel} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
@@ -12,6 +13,7 @@ import {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegrap
 import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/useFlamegraphPreferences';
 import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
 import {invertCallTree} from 'sentry/utils/profiling/profile/utils';
+import {useParams} from 'sentry/utils/useParams';
 
 import {FrameStackTable} from './frameStackTable';
 
@@ -25,6 +27,7 @@ interface FrameStackProps {
 }
 
 const FrameStack = memo(function FrameStack(props: FrameStackProps) {
+  const params = useParams();
   const [flamegraphPreferences, dispatchFlamegraphPreferences] =
     useFlamegraphPreferences();
 
@@ -94,7 +97,7 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
   return (
     <FrameDrawer layout={flamegraphPreferences.layout}>
       <FrameTabs>
-        <li className={tab === 'bottom up' ? 'active' : undefined}>
+        <ListItem className={tab === 'bottom up' ? 'active' : undefined}>
           <Button
             data-title={t('Bottom Up')}
             priority="link"
@@ -103,8 +106,8 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
           >
             {t('Bottom Up')}
           </Button>
-        </li>
-        <li className={tab === 'call order' ? 'active' : undefined}>
+        </ListItem>
+        <ListItem margin="none" className={tab === 'call order' ? 'active' : undefined}>
           <Button
             data-title={t('Call Order')}
             priority="link"
@@ -113,9 +116,9 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
           >
             {t('Call Order')}
           </Button>
-        </li>
+        </ListItem>
         <Separator />
-        <li className={treeType === 'all' ? 'active' : undefined}>
+        <ListItem className={treeType === 'all' ? 'active' : undefined}>
           <Button
             data-title={t('All Frames')}
             priority="link"
@@ -124,8 +127,8 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
           >
             {t('All Frames')}
           </Button>
-        </li>
-        <li className={treeType === 'application' ? 'active' : undefined}>
+        </ListItem>
+        <ListItem className={treeType === 'application' ? 'active' : undefined}>
           <Button
             data-title={t('Application Frames')}
             priority="link"
@@ -134,8 +137,8 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
           >
             {t('Application Frames')}
           </Button>
-        </li>
-        <li className={treeType === 'system' ? 'active' : undefined}>
+        </ListItem>
+        <ListItem margin="none" className={treeType === 'system' ? 'active' : undefined}>
           <Button
             data-title={t('System Frames')}
             priority="link"
@@ -144,9 +147,9 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
           >
             {t('System Frames')}
           </Button>
-        </li>
+        </ListItem>
         <Separator />
-        <li>
+        <ListItem>
           <FrameDrawerLabel>
             <input
               type="checkbox"
@@ -155,8 +158,8 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
             />
             {t('Collapse recursion')}
           </FrameDrawerLabel>
-        </li>
-        <li
+        </ListItem>
+        <ListItem
           style={{
             flex: '1 1 100%',
             cursor:
@@ -166,7 +169,20 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
             flamegraphPreferences.layout === 'table bottom' ? props.onResize : undefined
           }
         />
-        <li>
+        <ListItem margin="none">
+          <ExportProfileButton
+            eventId={params.eventId}
+            orgId={params.orgId}
+            projectId={params.projectId}
+            disabled={
+              params.eventId === undefined ||
+              params.orgId === undefined ||
+              params.projectId === undefined
+            }
+          />
+        </ListItem>
+        <Separator />
+        <ListItem>
           <LayoutSelectionContainer>
             <StyledButton
               active={flamegraphPreferences.layout === 'table left'}
@@ -193,7 +209,7 @@ const FrameStack = memo(function FrameStack(props: FrameStackProps) {
               <IconPanel size="xs" direction="left" />
             </StyledButton>
           </LayoutSelectionContainer>
-        </li>
+        </ListItem>
       </FrameTabs>
       <FrameStackTable
         {...props}
@@ -273,7 +289,7 @@ const FrameDrawer = styled('div')<{layout: FlamegraphPreferences['layout']}>`
 const Separator = styled('li')`
   width: 1px;
   height: 66%;
-  margin: 0 ${space(0.5)};
+  margin: 0 ${space(1)};
   background: ${p => p.theme.border};
   transform: translateY(29%);
 `;
@@ -287,41 +303,41 @@ const FrameTabs = styled('ul')`
   background-color: ${props => props.theme.surface400};
   user-select: none;
   grid-area: tabs;
+`;
 
-  > li {
-    font-size: ${p => p.theme.fontSizeSmall};
-    margin-right: ${space(1)};
-
-    button {
-      border: none;
-      border-top: 2px solid transparent;
-      border-bottom: 2px solid transparent;
-      border-radius: 0;
-      margin: 0;
-      padding: ${space(0.5)} 0;
-      color: ${p => p.theme.textColor};
+const ListItem = styled('li')<{margin?: 'none'}>`
+  font-size: ${p => p.theme.fontSizeSmall};
+  margin-right: ${p => (p.margin === 'none' ? 0 : space(1))};
 
-      &::after {
-        display: block;
-        content: attr(data-title);
-        font-weight: bold;
-        height: 1px;
-        color: transparent;
-        overflow: hidden;
-        visibility: hidden;
-        white-space: nowrap;
-      }
-
-      &:hover {
-        color: ${p => p.theme.textColor};
-      }
+  button {
+    border: none;
+    border-top: 2px solid transparent;
+    border-bottom: 2px solid transparent;
+    border-radius: 0;
+    margin: 0;
+    padding: ${space(0.5)} 0;
+    color: ${p => p.theme.textColor};
+
+    &::after {
+      display: block;
+      content: attr(data-title);
+      font-weight: bold;
+      height: 1px;
+      color: transparent;
+      overflow: hidden;
+      visibility: hidden;
+      white-space: nowrap;
     }
 
-    &.active button {
-      font-weight: bold;
-      border-bottom: 2px solid ${prop => prop.theme.active};
+    &:hover {
+      color: ${p => p.theme.textColor};
     }
   }
+
+  &.active button {
+    font-weight: bold;
+    border-bottom: 2px solid ${prop => prop.theme.active};
+  }
 `;
 
 const StyledButton = styled(Button)<{active: boolean}>`

+ 55 - 0
static/app/components/profiling/exportProfileButton.tsx

@@ -0,0 +1,55 @@
+import styled from '@emotion/styled';
+
+import {IconDownload} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
+
+import Button, {ButtonPropsWithoutAriaLabel} from '../button';
+
+interface ExportProfileButtonProps
+  extends Omit<ButtonPropsWithoutAriaLabel, 'onClick' | 'children'> {
+  eventId: string | undefined;
+  orgId: string | undefined;
+  projectId: string | undefined;
+}
+
+export function ExportProfileButton(props: ExportProfileButtonProps) {
+  const api = useApi();
+  const organization = useOrganization();
+
+  const project = useProjects().projects.find(p => {
+    return p.slug === props.projectId;
+  });
+
+  return (
+    <StyledButton
+      {...props}
+      size="xs"
+      title={t('Export Profile')}
+      href={`${api.baseUrl}/projects/${props.orgId}/${props.projectId}/profiling/raw_profiles/${props.eventId}/`}
+      download={`${organization.slug}_${
+        project?.slug ?? props.projectId ?? 'unknown_project'
+      }_${props.eventId}.profile.json`}
+    >
+      <IconDownload size="xs" />
+    </StyledButton>
+  );
+}
+
+const StyledButton = styled(Button)`
+  border: none;
+  background-color: transparent;
+  box-shadow: none;
+  transition: none !important;
+  opacity: 0.5;
+  padding: ${space(0.5)} ${space(0.5)};
+
+  &:hover {
+    border: none;
+    background-color: transparent;
+    box-shadow: none;
+  }
+`;