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)}; `;