trace.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import {useEffect, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  4. import LoadingError from 'sentry/components/loadingError';
  5. import LoadingIndicator from 'sentry/components/loadingIndicator';
  6. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  7. import {t} from 'sentry/locale';
  8. import {Organization} from 'sentry/types';
  9. import {getUtcDateString} from 'sentry/utils/dates';
  10. import {TableData} from 'sentry/utils/discover/discoverQuery';
  11. import EventView from 'sentry/utils/discover/eventView';
  12. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  13. import {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  14. import {
  15. getTraceRequestPayload,
  16. makeEventView,
  17. } from 'sentry/utils/performance/quickTrace/utils';
  18. import useApi from 'sentry/utils/useApi';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import TraceView from 'sentry/views/performance/traceDetails/traceView';
  21. import type {ReplayListLocationQuery, ReplayRecord} from 'sentry/views/replays/types';
  22. type State = {
  23. /**
  24. * Error, if not null.
  25. */
  26. // error: QueryError | null;
  27. error: any | null;
  28. /**
  29. * Loading state of this query.
  30. */
  31. isLoading: boolean;
  32. /**
  33. * Pagelinks, if applicable. Can be provided to the Pagination component.
  34. */
  35. pageLinks: string | null;
  36. /**
  37. * EventView that generates API payload
  38. */
  39. traceEventView: EventView | null;
  40. /**
  41. * Data / result.
  42. */
  43. traces: TraceFullDetailed[] | null;
  44. };
  45. interface Props {
  46. organization: Organization;
  47. replayRecord: ReplayRecord;
  48. }
  49. const INITIAL_STATE = Object.freeze({
  50. error: null,
  51. isLoading: true,
  52. pageLinks: null,
  53. traceEventView: null,
  54. traces: null,
  55. });
  56. export default function Trace({replayRecord, organization}: Props) {
  57. const [state, setState] = useState<State>(INITIAL_STATE);
  58. const api = useApi();
  59. const location = useLocation<ReplayListLocationQuery>();
  60. const replayId = replayRecord.id;
  61. const orgSlug = organization.slug;
  62. const start = getUtcDateString(replayRecord.startedAt.getTime());
  63. const end = getUtcDateString(replayRecord.finishedAt.getTime());
  64. useEffect(() => {
  65. async function loadTraces() {
  66. const eventView = EventView.fromSavedQuery({
  67. id: undefined,
  68. name: `Traces in replay ${replayId}`,
  69. fields: ['trace', 'count(trace)', 'min(timestamp)'],
  70. orderby: 'min_timestamp',
  71. query: `replayId:${replayId}`,
  72. projects: [ALL_ACCESS_PROJECTS],
  73. version: 2,
  74. start,
  75. end,
  76. });
  77. try {
  78. const [data, , resp] = await doDiscoverQuery<TableData>(
  79. api,
  80. `/organizations/${orgSlug}/events/`,
  81. eventView.getEventsAPIPayload(location)
  82. );
  83. const traceIds = data.data.map(({trace}) => trace).filter(trace => trace);
  84. // TODO(replays): Potential performance concerns here if number of traceIds is large
  85. const traceDetails = await Promise.allSettled(
  86. traceIds.map(traceId =>
  87. doDiscoverQuery(
  88. api,
  89. `/organizations/${orgSlug}/events-trace/${traceId}/`,
  90. getTraceRequestPayload({
  91. eventView: makeEventView({start, end}),
  92. location,
  93. })
  94. )
  95. )
  96. );
  97. const successfulTraceDetails = traceDetails
  98. .map(settled => (settled.status === 'fulfilled' ? settled.value[0] : undefined))
  99. .filter(Boolean);
  100. if (successfulTraceDetails.length !== traceDetails.length) {
  101. traceDetails.forEach(trace => {
  102. if (trace.status === 'rejected') {
  103. Sentry.captureMessage(trace.reason);
  104. }
  105. });
  106. }
  107. setState(prevState => ({
  108. isLoading: false,
  109. error: null,
  110. traceEventView: eventView,
  111. pageLinks: resp?.getResponseHeader('Link') ?? prevState.pageLinks,
  112. traces:
  113. successfulTraceDetails.flatMap(trace => trace as TraceFullDetailed[]) || [],
  114. }));
  115. } catch (err) {
  116. setState({
  117. isLoading: false,
  118. error: err,
  119. pageLinks: null,
  120. traceEventView: null,
  121. traces: null,
  122. });
  123. }
  124. }
  125. loadTraces();
  126. return () => {};
  127. }, [api, replayId, orgSlug, location, start, end]);
  128. if (state.isLoading) {
  129. return <LoadingIndicator />;
  130. }
  131. if (state.error || !state.traceEventView) {
  132. return <LoadingError />;
  133. }
  134. if (!state.traces?.length) {
  135. return (
  136. <EmptyStateWarning withIcon={false} small>
  137. {t('No traces found')}
  138. </EmptyStateWarning>
  139. );
  140. }
  141. return (
  142. <TraceView
  143. meta={null}
  144. traces={state.traces}
  145. location={location}
  146. organization={organization}
  147. traceEventView={state.traceEventView}
  148. traceSlug="Replay"
  149. />
  150. );
  151. // TODO(replays): pagination
  152. }