123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 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 (
- <EventComparisonListInner
- breakpoint={breakpoint}
- fingerprint={fingerprint}
- frameName={frameName}
- framePackage={framePackage}
- project={project}
- />
- );
- }
- 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 (
- <Wrapper>
- <EventDataSection type="profiles-before" title={t('Example Profiles Before')}>
- <EventList
- frameName={frameName}
- framePackage={framePackage}
- organization={organization}
- profiles={beforeProfiles}
- project={project}
- unit={durationUnit}
- />
- </EventDataSection>
- <EventDataSection type="profiles-after" title={t('Example Profiles After')}>
- <EventList
- frameName={frameName}
- framePackage={framePackage}
- organization={organization}
- profiles={afterProfiles}
- project={project}
- unit={durationUnit}
- />
- </EventDataSection>
- </Wrapper>
- );
- }
- 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 (
- <ListContainer>
- <Container>
- <strong>{t('Profile ID')}</strong>
- </Container>
- <Container>
- <strong>{t('Transaction')}</strong>
- </Container>
- <NumberContainer>
- <strong>{t('Duration')} </strong>
- <QuestionTooltip size="xs" position="top" title={t('The profile duration')} />
- </NumberContainer>
- {profiles.map(item => {
- const target = generateProfileFlamechartRouteWithQuery({
- orgSlug: organization.slug,
- projectSlug: project.slug,
- profileId: item['profile.id'],
- query: {
- frameName,
- framePackage,
- },
- });
- return (
- <Fragment key={item['profile.id']}>
- <Container>
- <Link
- to={target}
- onClick={() => {
- trackAnalytics('profiling_views.go_to_flamegraph', {
- organization,
- source: 'profiling.issue.function_regression.list',
- });
- }}
- >
- {getShortEventId(item['profile.id'])}
- </Link>
- </Container>
- <Container>{item.transaction}</Container>
- <NumberContainer>
- {unit === 'millisecond' ? (
- <PerformanceDuration
- milliseconds={item['transaction.duration']}
- abbreviation
- />
- ) : (
- <PerformanceDuration
- nanoseconds={item['transaction.duration']}
- abbreviation
- />
- )}
- </NumberContainer>
- </Fragment>
- );
- })}
- </ListContainer>
- );
- }
- 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)};
- `;
|