allEventsTable.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import {useEffect, useMemo, useState} from 'react';
  2. import type {Location} from 'history';
  3. import {getSampleEventQuery} from 'sentry/components/events/eventStatisticalDetector/eventComparison/eventDisplay';
  4. import LoadingError from 'sentry/components/loadingError';
  5. import {
  6. PlatformCategory,
  7. profiling as PROFILING_PLATFORMS,
  8. } from 'sentry/data/platformCategories';
  9. import {t} from 'sentry/locale';
  10. import type {EventTransaction} from 'sentry/types/event';
  11. import type {Group} from 'sentry/types/group';
  12. import {IssueCategory, IssueType} from 'sentry/types/group';
  13. import type {Organization} from 'sentry/types/organization';
  14. import type {PlatformKey} from 'sentry/types/project';
  15. import EventView from 'sentry/utils/discover/eventView';
  16. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  17. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  18. import {platformToCategory} from 'sentry/utils/platform';
  19. import {useApiQuery} from 'sentry/utils/queryClient';
  20. import {decodeSorts} from 'sentry/utils/queryString';
  21. import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
  22. import {useRoutes} from 'sentry/utils/useRoutes';
  23. import EventsTable from 'sentry/views/performance/transactionSummary/transactionEvents/eventsTable';
  24. interface Props {
  25. group: Group;
  26. issueId: string;
  27. location: Location;
  28. organization: Organization;
  29. excludedTags?: string[];
  30. }
  31. const makeGroupPreviewRequestUrl = ({groupId}: {groupId: string}) => {
  32. return `/issues/${groupId}/events/latest/`;
  33. };
  34. function AllEventsTable(props: Props) {
  35. const {location, organization, issueId, excludedTags, group} = props;
  36. const config = getConfigForIssueType(props.group, group.project);
  37. const [error, setError] = useState<string>('');
  38. const routes = useRoutes();
  39. const {fields, columnTitles} = useEventColumns(group, organization);
  40. const now = useMemo(() => Date.now(), []);
  41. const endpointUrl = makeGroupPreviewRequestUrl({
  42. groupId: group.id,
  43. });
  44. const queryEnabled = group.issueCategory === IssueCategory.PERFORMANCE;
  45. const {data, isPending, isLoadingError} = useApiQuery<EventTransaction>([endpointUrl], {
  46. staleTime: 60000,
  47. enabled: queryEnabled,
  48. });
  49. // TODO: this is a temporary way to check whether
  50. // perf issue is backed by occurrences or transactions
  51. // Once migration to the issue platform is complete a call to /latest should be removed
  52. const groupIsOccurrenceBacked = !!data?.occurrence;
  53. const eventView: EventView = EventView.fromLocation(props.location);
  54. if (
  55. config.usesIssuePlatform ||
  56. (group.issueCategory === IssueCategory.PERFORMANCE && groupIsOccurrenceBacked)
  57. ) {
  58. eventView.dataset = DiscoverDatasets.ISSUE_PLATFORM;
  59. }
  60. eventView.fields = fields.map(fieldName => ({field: fieldName}));
  61. eventView.sorts = decodeSorts(location.query.sort).filter(sort =>
  62. fields.includes(sort.field)
  63. );
  64. useEffect(() => {
  65. setError('');
  66. }, [eventView.query]);
  67. if (!eventView.sorts.length) {
  68. eventView.sorts = [{field: 'timestamp', kind: 'desc'}];
  69. }
  70. eventView.statsPeriod = '90d';
  71. const isRegressionIssue =
  72. group.issueType === IssueType.PERFORMANCE_DURATION_REGRESSION ||
  73. group.issueType === IssueType.PERFORMANCE_ENDPOINT_REGRESSION;
  74. let idQuery = `issue.id:${issueId}`;
  75. if (group.issueCategory === IssueCategory.PERFORMANCE && !groupIsOccurrenceBacked) {
  76. idQuery = `performance.issue_ids:${issueId} event.type:transaction`;
  77. } else if (isRegressionIssue && groupIsOccurrenceBacked) {
  78. const {transaction, aggregateRange2, breakpoint} =
  79. data?.occurrence?.evidenceData ?? {};
  80. // Surface the "bad" events that occur after the breakpoint
  81. idQuery = getSampleEventQuery({
  82. transaction,
  83. durationBaseline: aggregateRange2,
  84. addUpperBound: false,
  85. });
  86. eventView.dataset = DiscoverDatasets.DISCOVER;
  87. eventView.start = new Date(breakpoint * 1000).toISOString();
  88. eventView.end = new Date(now).toISOString();
  89. eventView.statsPeriod = undefined;
  90. }
  91. eventView.project = [parseInt(group.project.id, 10)];
  92. eventView.query = `${idQuery} ${props.location.query.query || ''}`;
  93. if (error || isLoadingError) {
  94. return (
  95. <LoadingError message={error || isLoadingError} onRetry={() => setError('')} />
  96. );
  97. }
  98. return (
  99. <EventsTable
  100. eventView={eventView}
  101. location={location}
  102. issueId={issueId}
  103. isRegressionIssue={isRegressionIssue}
  104. organization={organization}
  105. routes={routes}
  106. excludedTags={excludedTags}
  107. projectSlug={group.project.slug}
  108. customColumns={['minidump']}
  109. setError={(msg: string | undefined) => setError(msg ?? '')}
  110. transactionName=""
  111. columnTitles={columnTitles.slice()}
  112. referrer="api.issues.issue_events"
  113. isEventLoading={queryEnabled ? isPending : false}
  114. />
  115. );
  116. }
  117. type ColumnInfo = {columnTitles: string[]; fields: string[]};
  118. export const useEventColumns = (group: Group, organization: Organization): ColumnInfo => {
  119. return useMemo(() => {
  120. const isPerfIssue = group.issueCategory === IssueCategory.PERFORMANCE;
  121. const isReplayEnabled =
  122. organization.features.includes('session-replay') &&
  123. projectCanLinkToReplay(organization, group.project);
  124. // profiles only exist on transactions, so this only works with
  125. // performance issues, and not errors
  126. const isProfilingEnabled = isPerfIssue && organization.features.includes('profiling');
  127. const {fields: platformSpecificFields, columnTitles: platformSpecificColumnTitles} =
  128. getPlatformColumns(group.project.platform ?? group.platform, {
  129. isProfilingEnabled,
  130. isReplayEnabled,
  131. });
  132. const fields: string[] = [
  133. 'id',
  134. 'transaction',
  135. 'title',
  136. 'trace',
  137. 'timestamp',
  138. 'release',
  139. 'environment',
  140. 'user.display',
  141. 'device',
  142. 'os',
  143. ...platformSpecificFields,
  144. ...(isPerfIssue ? ['transaction.duration'] : []),
  145. ];
  146. const columnTitles: string[] = [
  147. t('Event ID'),
  148. t('Transaction'),
  149. t('Title'),
  150. t('Trace'),
  151. t('Timestamp'),
  152. t('Release'),
  153. t('Environment'),
  154. t('User'),
  155. t('Device'),
  156. t('OS'),
  157. ...platformSpecificColumnTitles,
  158. ...(isPerfIssue ? [t('Total Duration')] : []),
  159. t('Minidump'),
  160. ];
  161. return {
  162. fields,
  163. columnTitles,
  164. };
  165. }, [group, organization]);
  166. };
  167. const getPlatformColumns = (
  168. platform: PlatformKey | undefined,
  169. options: {isProfilingEnabled: boolean; isReplayEnabled: boolean}
  170. ): ColumnInfo => {
  171. const backendServerlessColumnInfo = {
  172. fields: ['url', 'runtime'],
  173. columnTitles: [t('URL'), t('Runtime')],
  174. };
  175. const categoryToColumnMap: Record<PlatformCategory, ColumnInfo> = {
  176. [PlatformCategory.BACKEND]: backendServerlessColumnInfo,
  177. [PlatformCategory.SERVERLESS]: backendServerlessColumnInfo,
  178. [PlatformCategory.FRONTEND]: {
  179. fields: ['url', 'browser'],
  180. columnTitles: [t('URL'), t('Browser')],
  181. },
  182. [PlatformCategory.MOBILE]: {
  183. fields: ['url'],
  184. columnTitles: [t('URL')],
  185. },
  186. [PlatformCategory.DESKTOP]: {
  187. fields: [],
  188. columnTitles: [],
  189. },
  190. [PlatformCategory.OTHER]: {
  191. fields: [],
  192. columnTitles: [],
  193. },
  194. };
  195. const platformCategory = platformToCategory(platform);
  196. const platformColumns = categoryToColumnMap[platformCategory];
  197. if (options.isReplayEnabled) {
  198. platformColumns.fields.push('replayId');
  199. platformColumns.columnTitles.push(t('Replay'));
  200. }
  201. if (options.isProfilingEnabled && platform && PROFILING_PLATFORMS.includes(platform)) {
  202. platformColumns.columnTitles.push(t('Profile'));
  203. platformColumns.fields.push('profile.id');
  204. }
  205. return platformColumns;
  206. };
  207. export default AllEventsTable;