|
@@ -0,0 +1,195 @@
|
|
|
|
+import {type ReactNode, useMemo} from 'react';
|
|
|
|
+import {ClassNames} from '@emotion/react';
|
|
|
|
+import styled from '@emotion/styled';
|
|
|
|
+
|
|
|
|
+import {Button, LinkButton} from 'sentry/components/button';
|
|
|
|
+import {Hovercard} from 'sentry/components/hovercard';
|
|
|
|
+import {platformsWithNestedInstrumentationGuides} from 'sentry/data/platformCategories';
|
|
|
|
+import {IconOpen, IconQuestion} from 'sentry/icons';
|
|
|
|
+import {t} from 'sentry/locale';
|
|
|
|
+import {space} from 'sentry/styles/space';
|
|
|
|
+import type {EventTransaction} from 'sentry/types/event';
|
|
|
|
+import type {Project} from 'sentry/types/project';
|
|
|
|
+import type {UseApiQueryResult} from 'sentry/utils/queryClient';
|
|
|
|
+import type RequestError from 'sentry/utils/requestError/requestError';
|
|
|
|
+import useOrganization from 'sentry/utils/useOrganization';
|
|
|
|
+import useProjects from 'sentry/utils/useProjects';
|
|
|
|
+import {traceAnalytics} from 'sentry/views/performance/newTraceDetails/traceAnalytics';
|
|
|
|
+
|
|
|
|
+function Resource({
|
|
|
|
+ title,
|
|
|
|
+ subtitle,
|
|
|
|
+ link,
|
|
|
|
+}: {
|
|
|
|
+ link: string;
|
|
|
|
+ subtitle: ReactNode;
|
|
|
|
+ title: string;
|
|
|
|
+}) {
|
|
|
|
+ const organization = useOrganization();
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <StyledLinkButton
|
|
|
|
+ icon={<IconOpen />}
|
|
|
|
+ borderless
|
|
|
|
+ external
|
|
|
|
+ href={link}
|
|
|
|
+ onClick={() => {
|
|
|
|
+ traceAnalytics.trackTraceConfigurationsDocsClicked(organization, title);
|
|
|
|
+ }}
|
|
|
|
+ >
|
|
|
|
+ <ButtonContent>
|
|
|
|
+ <ButtonTitle>{title}</ButtonTitle>
|
|
|
|
+ <ButtonSubtitle>{subtitle}</ButtonSubtitle>
|
|
|
|
+ </ButtonContent>
|
|
|
|
+ </StyledLinkButton>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type ParsedPlatform = {
|
|
|
|
+ platformName: string;
|
|
|
|
+ framework?: string;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+function parsePlatform(platform: string): ParsedPlatform {
|
|
|
|
+ // Except react-native, all other project platforms have the following two structures:
|
|
|
|
+ // 1. "{language}-{framework}", e.g. "javascript-nextjs"
|
|
|
|
+ // 2. "{language}", e.g. "python"
|
|
|
|
+ const [platformName, framework] =
|
|
|
|
+ platform === 'react-native' ? ['react-native', undefined] : platform.split('-');
|
|
|
|
+
|
|
|
|
+ return {platformName, framework};
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function getCustomInstrumentationLink(project: Project | undefined): string {
|
|
|
|
+ // Default to JavaScript guide if project or platform is not available
|
|
|
|
+ if (!project || !project.platform) {
|
|
|
|
+ return `https://docs.sentry.io/platforms/javascript/tracing/instrumentation/custom-instrumentation/`;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const {platformName, framework} = parsePlatform(project.platform);
|
|
|
|
+
|
|
|
|
+ return platformsWithNestedInstrumentationGuides.includes(project.platform) && framework
|
|
|
|
+ ? `https://docs.sentry.io/platforms/${platformName}/guides/${framework}/tracing/instrumentation/custom-instrumentation/`
|
|
|
|
+ : `https://docs.sentry.io/platforms/${platformName}/tracing/instrumentation/custom-instrumentation/`;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function getDistributedTracingLink(project: Project | undefined): string {
|
|
|
|
+ // Default to JavaScript guide if project or platform is not available
|
|
|
|
+ if (!project || !project.platform) {
|
|
|
|
+ return `https://docs.sentry.io/platforms/javascript/tracing/trace-propagation/`;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const {platformName, framework} = parsePlatform(project.platform);
|
|
|
|
+
|
|
|
|
+ return framework
|
|
|
|
+ ? `https://docs.sentry.io/platforms/${platformName}/guides/${framework}/tracing/trace-propagation/`
|
|
|
|
+ : `https://docs.sentry.io/platforms/${platformName}/tracing/trace-propagation/`;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type ResourceButtonsProps = {
|
|
|
|
+ customInstrumentationLink: string;
|
|
|
|
+ distributedTracingLink: string;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+function ResourceButtons({
|
|
|
|
+ customInstrumentationLink,
|
|
|
|
+ distributedTracingLink,
|
|
|
|
+}: ResourceButtonsProps) {
|
|
|
|
+ return (
|
|
|
|
+ <ButtonContainer>
|
|
|
|
+ <Resource
|
|
|
|
+ title={t('Custom Instrumentation')}
|
|
|
|
+ subtitle={t('Add Custom Spans or Transactions to your traces')}
|
|
|
|
+ link={customInstrumentationLink}
|
|
|
|
+ />
|
|
|
|
+ <Resource
|
|
|
|
+ title={t('Distributed Tracing')}
|
|
|
|
+ subtitle={t('See the whole trace across all your services')}
|
|
|
|
+ link={distributedTracingLink}
|
|
|
|
+ />
|
|
|
|
+ </ButtonContainer>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type TraceConfigurationsProps = {
|
|
|
|
+ rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+export default function TraceConfigurations({
|
|
|
|
+ rootEventResults,
|
|
|
|
+}: TraceConfigurationsProps) {
|
|
|
|
+ const {projects} = useProjects();
|
|
|
|
+
|
|
|
|
+ const traceProject = useMemo(() => {
|
|
|
|
+ return rootEventResults.data
|
|
|
|
+ ? projects.find(p => p.id === rootEventResults.data.projectID)
|
|
|
|
+ : undefined;
|
|
|
|
+ }, [projects, rootEventResults.data]);
|
|
|
|
+
|
|
|
|
+ const customInstrumentationLink = useMemo(
|
|
|
|
+ () => getCustomInstrumentationLink(traceProject),
|
|
|
|
+ [traceProject]
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ const distributedTracingLink = useMemo(
|
|
|
|
+ () => getDistributedTracingLink(traceProject),
|
|
|
|
+ [traceProject]
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ return (
|
|
|
|
+ <ClassNames>
|
|
|
|
+ {({css}) => (
|
|
|
|
+ <Hovercard
|
|
|
|
+ body={
|
|
|
|
+ <ResourceButtons
|
|
|
|
+ customInstrumentationLink={customInstrumentationLink}
|
|
|
|
+ distributedTracingLink={distributedTracingLink}
|
|
|
|
+ />
|
|
|
|
+ }
|
|
|
|
+ bodyClassName={css`
|
|
|
|
+ padding: ${space(1)};
|
|
|
|
+ `}
|
|
|
|
+ position="top-end"
|
|
|
|
+ >
|
|
|
|
+ <Button
|
|
|
|
+ size="sm"
|
|
|
|
+ icon={<IconQuestion />}
|
|
|
|
+ aria-label={t('trace configure resources')}
|
|
|
|
+ >
|
|
|
|
+ {t('Configure Traces')}
|
|
|
|
+ </Button>
|
|
|
|
+ </Hovercard>
|
|
|
|
+ )}
|
|
|
|
+ </ClassNames>
|
|
|
|
+ );
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const ButtonContainer = styled('div')`
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ gap: ${space(1)};
|
|
|
|
+ align-items: flex-start;
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ButtonContent = styled('div')`
|
|
|
|
+ display: flex;
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ text-align: left;
|
|
|
|
+ white-space: pre-line;
|
|
|
|
+ gap: ${space(0.25)};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ButtonTitle = styled('div')`
|
|
|
|
+ font-weight: ${p => p.theme.fontWeightNormal};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const ButtonSubtitle = styled('div')`
|
|
|
|
+ color: ${p => p.theme.gray300};
|
|
|
|
+ font-weight: ${p => p.theme.fontWeightNormal};
|
|
|
|
+ font-size: ${p => p.theme.fontSizeSmall};
|
|
|
|
+`;
|
|
|
|
+
|
|
|
|
+const StyledLinkButton = styled(LinkButton)`
|
|
|
|
+ padding: ${space(1)};
|
|
|
|
+ height: auto;
|
|
|
|
+`;
|