eventFunctionComparisonList.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import {Fragment, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  5. import Link from 'sentry/components/links/link';
  6. import PerformanceDuration from 'sentry/components/performanceDuration';
  7. import QuestionTooltip from 'sentry/components/questionTooltip';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {Event, Group, Organization, Project} from 'sentry/types';
  11. import {defined} from 'sentry/utils';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import {Container, NumberContainer} from 'sentry/utils/discover/styles';
  14. import {getShortEventId} from 'sentry/utils/events';
  15. import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
  16. import {useProfileFunctions} from 'sentry/utils/profiling/hooks/useProfileFunctions';
  17. import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime';
  18. import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. interface EventFunctionComparisonListProps {
  21. event: Event;
  22. group: Group;
  23. project: Project;
  24. }
  25. export function EventFunctionComparisonList({
  26. event,
  27. project,
  28. }: EventFunctionComparisonListProps) {
  29. const evidenceData = event.occurrence?.evidenceData;
  30. const fingerprint = evidenceData?.fingerprint;
  31. const breakpoint = evidenceData?.breakpoint;
  32. const frameName = evidenceData?.function;
  33. const framePackage = evidenceData?.package || evidenceData?.module;
  34. const isValid =
  35. defined(fingerprint) &&
  36. defined(breakpoint) &&
  37. defined(frameName) &&
  38. defined(framePackage);
  39. useEffect(() => {
  40. if (isValid) {
  41. return;
  42. }
  43. Sentry.withScope(scope => {
  44. scope.setContext('evidence data fields', {
  45. fingerprint,
  46. breakpoint,
  47. frameName,
  48. framePackage,
  49. });
  50. Sentry.captureException(
  51. new Error('Missing required evidence data on function regression issue.')
  52. );
  53. });
  54. }, [isValid, fingerprint, breakpoint, frameName, framePackage]);
  55. if (!isValid) {
  56. return null;
  57. }
  58. return (
  59. <EventComparisonListInner
  60. breakpoint={breakpoint}
  61. fingerprint={fingerprint}
  62. frameName={frameName}
  63. framePackage={framePackage}
  64. project={project}
  65. />
  66. );
  67. }
  68. interface EventComparisonListInnerProps {
  69. breakpoint: number;
  70. fingerprint: number;
  71. frameName: string;
  72. framePackage: string;
  73. project: Project;
  74. }
  75. function EventComparisonListInner({
  76. breakpoint,
  77. fingerprint,
  78. frameName,
  79. framePackage,
  80. project,
  81. }: EventComparisonListInnerProps) {
  82. const organization = useOrganization();
  83. const breakpointDateTime = new Date(breakpoint * 1000);
  84. const datetime = useRelativeDateTime({
  85. anchor: breakpoint,
  86. relativeDays: 1,
  87. });
  88. const {start: beforeDateTime, end: afterDateTime} = datetime;
  89. const beforeProfilesQuery = useProfileFunctions({
  90. datetime: {
  91. start: beforeDateTime,
  92. end: breakpointDateTime,
  93. utc: true,
  94. period: null,
  95. },
  96. fields: ['examples()'],
  97. sort: {
  98. key: 'examples()',
  99. order: 'asc',
  100. },
  101. query: `fingerprint:${fingerprint}`,
  102. projects: [project.id],
  103. limit: 1,
  104. referrer: 'api.profiling.functions.regression.list',
  105. });
  106. const afterProfilesQuery = useProfileFunctions({
  107. datetime: {
  108. start: breakpointDateTime,
  109. end: afterDateTime,
  110. utc: true,
  111. period: null,
  112. },
  113. fields: ['examples()'],
  114. sort: {
  115. key: 'examples()',
  116. order: 'asc',
  117. },
  118. query: `fingerprint:${fingerprint}`,
  119. projects: [project.id],
  120. limit: 1,
  121. referrer: 'api.profiling.functions.regression.list',
  122. });
  123. const beforeProfileIds =
  124. (beforeProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? [];
  125. const afterProfileIds =
  126. (afterProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? [];
  127. const profilesQuery = useProfileEvents({
  128. datetime,
  129. fields: ['profile.id', 'transaction', 'transaction.duration'],
  130. query: `profile.id:[${[...beforeProfileIds, ...afterProfileIds].join(', ')}]`,
  131. sort: {
  132. key: 'transaction.duration',
  133. order: 'desc',
  134. },
  135. projects: [project.id],
  136. limit: beforeProfileIds.length + afterProfileIds.length,
  137. enabled: beforeProfileIds.length > 0 && afterProfileIds.length > 0,
  138. referrer: 'api.profiling.functions.regression.examples',
  139. });
  140. const beforeProfiles = useMemo(() => {
  141. const profileIds = new Set(
  142. (beforeProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? []
  143. );
  144. return (
  145. (profilesQuery.data?.data?.filter(row =>
  146. profileIds.has(row['profile.id'] as string)
  147. ) as ProfileItem[]) ?? []
  148. );
  149. }, [beforeProfilesQuery, profilesQuery]);
  150. const afterProfiles = useMemo(() => {
  151. const profileIds = new Set(
  152. (afterProfilesQuery.data?.data?.[0]?.['examples()'] as string[]) ?? []
  153. );
  154. return (
  155. (profilesQuery.data?.data?.filter(row =>
  156. profileIds.has(row['profile.id'] as string)
  157. ) as ProfileItem[]) ?? []
  158. );
  159. }, [afterProfilesQuery, profilesQuery]);
  160. const durationUnit = profilesQuery.data?.meta?.units?.['transaction.duration'] ?? '';
  161. return (
  162. <Wrapper>
  163. <EventDataSection type="profiles-before" title={t('Example Profiles Before')}>
  164. <EventList
  165. frameName={frameName}
  166. framePackage={framePackage}
  167. organization={organization}
  168. profiles={beforeProfiles}
  169. project={project}
  170. unit={durationUnit}
  171. />
  172. </EventDataSection>
  173. <EventDataSection type="profiles-after" title={t('Example Profiles After')}>
  174. <EventList
  175. frameName={frameName}
  176. framePackage={framePackage}
  177. organization={organization}
  178. profiles={afterProfiles}
  179. project={project}
  180. unit={durationUnit}
  181. />
  182. </EventDataSection>
  183. </Wrapper>
  184. );
  185. }
  186. interface ProfileItem {
  187. 'profile.id': string;
  188. timestamp: string;
  189. transaction: string;
  190. 'transaction.duration': number;
  191. }
  192. interface EventListProps {
  193. frameName: string;
  194. framePackage: string;
  195. organization: Organization;
  196. profiles: ProfileItem[];
  197. project: Project;
  198. unit: string;
  199. }
  200. function EventList({
  201. frameName,
  202. framePackage,
  203. organization,
  204. profiles,
  205. project,
  206. unit,
  207. }: EventListProps) {
  208. return (
  209. <ListContainer>
  210. <Container>
  211. <strong>{t('Profile ID')}</strong>
  212. </Container>
  213. <Container>
  214. <strong>{t('Transaction')}</strong>
  215. </Container>
  216. <NumberContainer>
  217. <strong>{t('Duration')} </strong>
  218. <QuestionTooltip size="xs" position="top" title={t('The profile duration')} />
  219. </NumberContainer>
  220. {profiles.map(item => {
  221. const target = generateProfileFlamechartRouteWithQuery({
  222. orgSlug: organization.slug,
  223. projectSlug: project.slug,
  224. profileId: item['profile.id'],
  225. query: {
  226. frameName,
  227. framePackage,
  228. },
  229. });
  230. return (
  231. <Fragment key={item['profile.id']}>
  232. <Container>
  233. <Link
  234. to={target}
  235. onClick={() => {
  236. trackAnalytics('profiling_views.go_to_flamegraph', {
  237. organization,
  238. source: 'profiling.issue.function_regression.list',
  239. });
  240. }}
  241. >
  242. {getShortEventId(item['profile.id'])}
  243. </Link>
  244. </Container>
  245. <Container>{item.transaction}</Container>
  246. <NumberContainer>
  247. {unit === 'millisecond' ? (
  248. <PerformanceDuration
  249. milliseconds={item['transaction.duration']}
  250. abbreviation
  251. />
  252. ) : (
  253. <PerformanceDuration
  254. nanoseconds={item['transaction.duration']}
  255. abbreviation
  256. />
  257. )}
  258. </NumberContainer>
  259. </Fragment>
  260. );
  261. })}
  262. </ListContainer>
  263. );
  264. }
  265. const Wrapper = styled('div')`
  266. display: grid;
  267. grid-template-columns: 1fr;
  268. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  269. grid-template-columns: 1fr 1fr;
  270. }
  271. `;
  272. const ListContainer = styled('div')`
  273. display: grid;
  274. grid-template-columns: minmax(75px, 1fr) auto minmax(75px, 1fr);
  275. gap: ${space(1)};
  276. `;