|
@@ -1,8 +1,10 @@
|
|
import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
|
|
import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
|
|
import {createPortal} from 'react-dom';
|
|
import {createPortal} from 'react-dom';
|
|
import {usePopper} from 'react-popper';
|
|
import {usePopper} from 'react-popper';
|
|
|
|
+import styled from '@emotion/styled';
|
|
|
|
|
|
import Link from 'sentry/components/links/link';
|
|
import Link from 'sentry/components/links/link';
|
|
|
|
+import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import {Flex} from 'sentry/components/profiling/flex';
|
|
import {Flex} from 'sentry/components/profiling/flex';
|
|
import {
|
|
import {
|
|
ProfilingContextMenu,
|
|
ProfilingContextMenu,
|
|
@@ -14,8 +16,6 @@ import {
|
|
} from 'sentry/components/profiling/profilingContextMenu';
|
|
} from 'sentry/components/profiling/profilingContextMenu';
|
|
import {IconChevron, IconCopy, IconGithub, IconProfiling} from 'sentry/icons';
|
|
import {IconChevron, IconCopy, IconGithub, IconProfiling} from 'sentry/icons';
|
|
import {t} from 'sentry/locale';
|
|
import {t} from 'sentry/locale';
|
|
-import {RequestState} from 'sentry/types';
|
|
|
|
-import {StacktraceLinkResult} from 'sentry/types/integrations';
|
|
|
|
import {defined} from 'sentry/utils';
|
|
import {defined} from 'sentry/utils';
|
|
import {getShortEventId} from 'sentry/utils/events';
|
|
import {getShortEventId} from 'sentry/utils/events';
|
|
import {
|
|
import {
|
|
@@ -27,9 +27,9 @@ import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/
|
|
import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
|
|
import {useDispatchFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
|
|
import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
|
|
import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
|
|
import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
|
|
import {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu';
|
|
|
|
+import {useSourceCodeLink} from 'sentry/utils/profiling/hooks/useSourceLink';
|
|
import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
|
|
import {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
|
|
import {generateProfileFlamechartRouteWithHighlightFrame} from 'sentry/utils/profiling/routes';
|
|
import {generateProfileFlamechartRouteWithHighlightFrame} from 'sentry/utils/profiling/routes';
|
|
-import useApi from 'sentry/utils/useApi';
|
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
|
|
|
|
@@ -66,78 +66,36 @@ function isSupportedPlatformForGitHubLink(platform: string | undefined): boolean
|
|
return false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
- return platform === 'node' || platform === 'python';
|
|
|
|
|
|
+ return platform.includes('javascript') || platform === 'node' || platform === 'python';
|
|
}
|
|
}
|
|
|
|
|
|
export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
|
|
export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
|
|
- const api = useApi();
|
|
|
|
const {projects} = useProjects();
|
|
const {projects} = useProjects();
|
|
const organization = useOrganization();
|
|
const organization = useOrganization();
|
|
const preferences = useFlamegraphPreferences();
|
|
const preferences = useFlamegraphPreferences();
|
|
const dispatch = useDispatchFlamegraphState();
|
|
const dispatch = useDispatchFlamegraphState();
|
|
|
|
|
|
- const [githubLink, setGithubLinkState] = useState<RequestState<StacktraceLinkResult>>({
|
|
|
|
- type: 'initial',
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
const project = projects.find(
|
|
const project = projects.find(
|
|
p => p.id === String(props.profileGroup?.metadata?.projectID)
|
|
p => p.id === String(props.profileGroup?.metadata?.projectID)
|
|
);
|
|
);
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
- if (!props.hoveredNode || !props.profileGroup) {
|
|
|
|
- return setGithubLinkState({type: 'initial'});
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (
|
|
|
|
- !project ||
|
|
|
|
- !props.hoveredNode ||
|
|
|
|
- // the profile ids indicate this is an aggregate flamegraph
|
|
|
|
- // and they do not support source code links
|
|
|
|
- props.hoveredNode.profileIds
|
|
|
|
- ) {
|
|
|
|
- return undefined;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (!isSupportedPlatformForGitHubLink(props.profileGroup.metadata.platform)) {
|
|
|
|
- return setGithubLinkState({type: 'errored', error: 'Unsupported platform'});
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- const metadata = props.profileGroup.metadata;
|
|
|
|
- const commitId = metadata.release?.lastCommit?.id;
|
|
|
|
- const platform = metadata.platform;
|
|
|
|
-
|
|
|
|
- const frame = props.hoveredNode.frame;
|
|
|
|
-
|
|
|
|
- setGithubLinkState({type: 'loading'});
|
|
|
|
-
|
|
|
|
- api
|
|
|
|
- .requestPromise(`/projects/${organization.slug}/${project.slug}/stacktrace-link/`, {
|
|
|
|
- query: {
|
|
|
|
- file: frame.file,
|
|
|
|
- platform,
|
|
|
|
- commitId,
|
|
|
|
- ...(frame.path && {absPath: frame.path}),
|
|
|
|
- },
|
|
|
|
- })
|
|
|
|
- .then(response => {
|
|
|
|
- setGithubLinkState({type: 'resolved', data: response});
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- return () => {
|
|
|
|
- api.clear();
|
|
|
|
- };
|
|
|
|
- }, [props.hoveredNode, api, project, organization, props.profileGroup]);
|
|
|
|
|
|
+ const sourceCodeLink = useSourceCodeLink({
|
|
|
|
+ project,
|
|
|
|
+ organization,
|
|
|
|
+ commitId: props.profileGroup?.metadata?.release?.lastCommit?.id,
|
|
|
|
+ platform: props.profileGroup?.metadata?.platform,
|
|
|
|
+ frame: {file: props.hoveredNode?.frame.file, path: props.hoveredNode?.frame.path},
|
|
|
|
+ });
|
|
|
|
|
|
// @TODO: this only works for github right now, other providers will not work
|
|
// @TODO: this only works for github right now, other providers will not work
|
|
const onOpenInGithubClick = useCallback(() => {
|
|
const onOpenInGithubClick = useCallback(() => {
|
|
- if (githubLink.type !== 'resolved') {
|
|
|
|
|
|
+ if (!sourceCodeLink.isSuccess) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
if (
|
|
if (
|
|
- !githubLink.data.sourceUrl ||
|
|
|
|
- githubLink.data.config?.provider?.key !== 'github'
|
|
|
|
|
|
+ !sourceCodeLink.data.sourceUrl ||
|
|
|
|
+ sourceCodeLink.data.config?.provider?.key !== 'github'
|
|
) {
|
|
) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
@@ -145,11 +103,11 @@ export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
|
|
// make a best effort to link to the exact line if we can
|
|
// make a best effort to link to the exact line if we can
|
|
const url =
|
|
const url =
|
|
defined(props.hoveredNode) && defined(props.hoveredNode.frame.line)
|
|
defined(props.hoveredNode) && defined(props.hoveredNode.frame.line)
|
|
- ? `${githubLink.data.sourceUrl}#L${props.hoveredNode.frame.line}`
|
|
|
|
- : githubLink.data.sourceUrl;
|
|
|
|
|
|
+ ? `${sourceCodeLink.data.sourceUrl}#L${props.hoveredNode.frame.line}`
|
|
|
|
+ : sourceCodeLink.data.sourceUrl;
|
|
|
|
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
- }, [props.hoveredNode, githubLink]);
|
|
|
|
|
|
+ }, [props.hoveredNode, sourceCodeLink]);
|
|
|
|
|
|
return props.contextMenu.open ? (
|
|
return props.contextMenu.open ? (
|
|
<Fragment>
|
|
<Fragment>
|
|
@@ -211,31 +169,32 @@ export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
|
|
>
|
|
>
|
|
{t('Copy source location')}
|
|
{t('Copy source location')}
|
|
</ProfilingContextMenuItemButton>
|
|
</ProfilingContextMenuItemButton>
|
|
- {githubLink.type !== 'initial' && (
|
|
|
|
- <ProfilingContextMenuItemButton
|
|
|
|
- disabled={
|
|
|
|
- githubLink.type !== 'resolved' ||
|
|
|
|
- !(githubLink.type === 'resolved' && githubLink.data.sourceUrl)
|
|
|
|
- }
|
|
|
|
- tooltip={
|
|
|
|
- !isSupportedPlatformForGitHubLink(
|
|
|
|
- props.profileGroup?.metadata?.platform
|
|
|
|
- )
|
|
|
|
- ? t('Open in GitHub is not yet supported for this platform')
|
|
|
|
- : githubLink.type === 'resolved' &&
|
|
|
|
- (!githubLink.data.sourceUrl ||
|
|
|
|
- githubLink.data.config?.provider?.key !== 'github')
|
|
|
|
- ? t('Could not find source code location in GitHub')
|
|
|
|
- : undefined
|
|
|
|
- }
|
|
|
|
- {...props.contextMenu.getMenuItemProps({
|
|
|
|
- onClick: onOpenInGithubClick,
|
|
|
|
- })}
|
|
|
|
- icon={<IconGithub size="xs" />}
|
|
|
|
- >
|
|
|
|
- {t('Open in GitHub')}
|
|
|
|
- </ProfilingContextMenuItemButton>
|
|
|
|
- )}
|
|
|
|
|
|
+ <ProfilingContextMenuItemButton
|
|
|
|
+ disabled={!sourceCodeLink.isSuccess || !sourceCodeLink.data?.sourceUrl}
|
|
|
|
+ tooltip={
|
|
|
|
+ !isSupportedPlatformForGitHubLink(props.profileGroup?.metadata?.platform)
|
|
|
|
+ ? t('Open in GitHub is not supported for this platform')
|
|
|
|
+ : sourceCodeLink.isLoading
|
|
|
|
+ ? 'Resolving link'
|
|
|
|
+ : sourceCodeLink.isSuccess &&
|
|
|
|
+ (!sourceCodeLink.data.sourceUrl ||
|
|
|
|
+ sourceCodeLink.data.config?.provider?.key !== 'github')
|
|
|
|
+ ? t('Could not find source code location in GitHub')
|
|
|
|
+ : undefined
|
|
|
|
+ }
|
|
|
|
+ {...props.contextMenu.getMenuItemProps({
|
|
|
|
+ onClick: onOpenInGithubClick,
|
|
|
|
+ })}
|
|
|
|
+ icon={
|
|
|
|
+ sourceCodeLink.isLoading ? (
|
|
|
|
+ <StyledLoadingIndicator size={10} hideMessage />
|
|
|
|
+ ) : (
|
|
|
|
+ <IconGithub size="xs" />
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ >
|
|
|
|
+ {t('Open in GitHub')}
|
|
|
|
+ </ProfilingContextMenuItemButton>
|
|
</ProfilingContextMenuGroup>
|
|
</ProfilingContextMenuGroup>
|
|
) : null}
|
|
) : null}
|
|
{props.disableColorCoding ? null : (
|
|
{props.disableColorCoding ? null : (
|
|
@@ -296,6 +255,16 @@ export function FlamegraphContextMenu(props: FlamegraphContextMenuProps) {
|
|
) : null;
|
|
) : null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+const StyledLoadingIndicator = styled(LoadingIndicator)`
|
|
|
|
+ margin: 0;
|
|
|
|
+ transform: translateX(-2px);
|
|
|
|
+
|
|
|
|
+ > div {
|
|
|
|
+ border: 2px solid ${p => p.theme.gray100} !important;
|
|
|
|
+ border-left-color: ${p => p.theme.gray200} !important;
|
|
|
|
+ }
|
|
|
|
+`;
|
|
|
|
+
|
|
function ProfileIdsSubMenu(props: {
|
|
function ProfileIdsSubMenu(props: {
|
|
contextMenu: FlamegraphContextMenuProps['contextMenu'];
|
|
contextMenu: FlamegraphContextMenuProps['contextMenu'];
|
|
frameName: string;
|
|
frameName: string;
|