import {Fragment, useEffect, useMemo} from 'react';
import styled from '@emotion/styled';
import * as Sentry from '@sentry/react';
import {EventDataSection} from 'sentry/components/events/eventDataSection';
import Link from 'sentry/components/links/link';
import PerformanceDuration from 'sentry/components/performanceDuration';
import QuestionTooltip from 'sentry/components/questionTooltip';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event, Group, Organization, Project} from 'sentry/types';
import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {Container, NumberContainer} from 'sentry/utils/discover/styles';
import {getShortEventId} from 'sentry/utils/events';
import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime';
import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
import useOrganization from 'sentry/utils/useOrganization';
interface EventFunctionComparisonListProps {
event: Event;
group: Group;
project: Project;
}
export function EventFunctionComparisonList({
event,
project,
}: EventFunctionComparisonListProps) {
const evidenceData = event.occurrence?.evidenceData;
const fingerprint = evidenceData?.fingerprint;
const breakpoint = evidenceData?.breakpoint;
const frameName = evidenceData?.function;
const framePackage = evidenceData?.package || evidenceData?.module;
const isValid =
defined(fingerprint) &&
defined(breakpoint) &&
defined(frameName) &&
defined(framePackage);
useEffect(() => {
if (isValid) {
return;
}
Sentry.withScope(scope => {
scope.setContext('evidence data fields', {
fingerprint,
breakpoint,
frameName,
framePackage,
});
Sentry.captureException(
new Error('Missing required evidence data on function regression issue.')
);
});
}, [isValid, fingerprint, breakpoint, frameName, framePackage]);
if (!isValid) {
return null;
}
return (
);
}
interface EventComparisonListInnerProps {
breakpoint: number;
fingerprint: number;
frameName: string;
framePackage: string;
project: Project;
}
function EventComparisonListInner({
breakpoint,
fingerprint,
frameName,
framePackage,
project,
}: EventComparisonListInnerProps) {
const organization = useOrganization();
const breakpointDateTime = new Date(breakpoint * 1000);
const datetime = useRelativeDateTime({
anchor: breakpoint,
relativeDays: 1,
});
const {start: beforeDateTime, end: afterDateTime} = datetime;
const beforeProfilesQuery = useProfileFunctions({
datetime: {
start: beforeDateTime,
end: breakpointDateTime,
utc: true,
period: null,
},
fields: ['examples()'],
sort: {
key: 'examples()',
order: 'asc',
},
query: `fingerprint:${fingerprint}`,
projects: [project.id],
limit: 1,
referrer: 'api.profiling.functions.regression.list',
});
const afterProfilesQuery = useProfileFunctions({
datetime: {
start: breakpointDateTime,
end: afterDateTime,
utc: true,
period: null,
},
fields: ['examples()'],
sort: {
key: 'examples()',
order: 'asc',
},
query: `fingerprint:${fingerprint}`,
projects: [project.id],
limit: 1,
referrer: 'api.profiling.functions.regression.list',
});
const beforeProfileIds =
(beforeProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? [];
const afterProfileIds =
(afterProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? [];
const profilesQuery = useProfileEvents({
datetime,
fields: ['profile.id', 'transaction', 'transaction.duration'],
query: `profile.id:[${[...beforeProfileIds, ...afterProfileIds].join(', ')}]`,
sort: {
key: 'transaction.duration',
order: 'desc',
},
projects: [project.id],
limit: beforeProfileIds.length + afterProfileIds.length,
enabled: beforeProfileIds.length > 0 && afterProfileIds.length > 0,
referrer: 'api.profiling.functions.regression.examples',
});
const beforeProfiles = useMemo(() => {
const profileIds = new Set(
(beforeProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? []
);
return (
(profilesQuery.data?.data?.filter(row =>
profileIds.has(row['profile.id'] as string)
) as ProfileItem[]) ?? []
);
}, [beforeProfilesQuery, profilesQuery]);
const afterProfiles = useMemo(() => {
const profileIds = new Set(
(afterProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? []
);
return (
(profilesQuery.data?.data?.filter(row =>
profileIds.has(row['profile.id'] as string)
) as ProfileItem[]) ?? []
);
}, [afterProfilesQuery, profilesQuery]);
const durationUnit = profilesQuery.data?.meta?.units?.['transaction.duration'] ?? '';
return (
);
}
interface ProfileItem {
'profile.id': string;
timestamp: string;
transaction: string;
'transaction.duration': number;
}
interface EventListProps {
frameName: string;
framePackage: string;
organization: Organization;
profiles: ProfileItem[];
project: Project;
unit: string;
}
function EventList({
frameName,
framePackage,
organization,
profiles,
project,
unit,
}: EventListProps) {
return (
{t('Profile ID')}
{t('Transaction')}
{t('Duration')}
{profiles.map(item => {
const target = generateProfileFlamechartRouteWithQuery({
orgSlug: organization.slug,
projectSlug: project.slug,
profileId: item['profile.id'],
query: {
frameName,
framePackage,
},
});
return (
{
trackAnalytics('profiling_views.go_to_flamegraph', {
organization,
source: 'profiling.issue.function_regression.list',
});
}}
>
{getShortEventId(item['profile.id'])}
{item.transaction}
{unit === 'millisecond' ? (
) : (
)}
);
})}
);
}
const Wrapper = styled('div')`
display: grid;
grid-template-columns: 1fr;
@media (min-width: ${p => p.theme.breakpoints.medium}) {
grid-template-columns: 1fr 1fr;
}
`;
const ListContainer = styled('div')`
display: grid;
grid-template-columns: minmax(75px, 1fr) auto minmax(75px, 1fr);
gap: ${space(1)};
`;