replayTransactionContext.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import type {ReactNode} from 'react';
  2. import {
  3. createContext,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useState,
  9. } from 'react';
  10. import type {Location} from 'history';
  11. import sortBy from 'lodash/sortBy';
  12. import {getTimeStampFromTableDateField, getUtcDateString} from 'sentry/utils/dates';
  13. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  14. import EventView from 'sentry/utils/discover/eventView';
  15. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  16. import type {ParsedHeader} from 'sentry/utils/parseLinkHeader';
  17. import parseLinkHeader from 'sentry/utils/parseLinkHeader';
  18. import type {
  19. TraceError,
  20. TraceFullDetailed,
  21. TraceSplitResults,
  22. } from 'sentry/utils/performance/quickTrace/types';
  23. import {
  24. getTraceRequestPayload,
  25. makeEventView,
  26. } from 'sentry/utils/performance/quickTrace/utils';
  27. import useEmitTimestampChanges from 'sentry/utils/replays/playback/hooks/useEmitTimestampChanges';
  28. import useApi from 'sentry/utils/useApi';
  29. import useOrganization from 'sentry/utils/useOrganization';
  30. import {getTraceSplitResults} from 'sentry/views/performance/traceDetails/utils';
  31. import type {ReplayRecord} from 'sentry/views/replays/types';
  32. type Options = {
  33. children: ReactNode;
  34. replayRecord: undefined | ReplayRecord;
  35. };
  36. type InternalState = {
  37. detailsErrors: Error[];
  38. detailsRequests: number;
  39. detailsResponses: number;
  40. didInit: boolean;
  41. indexComplete: boolean;
  42. indexError: undefined | Error;
  43. isFetching: boolean;
  44. traces: undefined | TraceFullDetailed[];
  45. orphanErrors?: TraceError[];
  46. };
  47. export type ExternalState = {
  48. didInit: boolean;
  49. errors: Error[];
  50. isFetching: boolean;
  51. traces: undefined | TraceFullDetailed[];
  52. orphanErrors?: TraceError[];
  53. };
  54. const INITIAL_STATE: InternalState = {
  55. detailsErrors: [],
  56. detailsRequests: 0,
  57. detailsResponses: 0,
  58. didInit: false,
  59. indexComplete: true,
  60. indexError: undefined,
  61. isFetching: false,
  62. traces: undefined,
  63. orphanErrors: undefined,
  64. };
  65. type TxnContextProps = {
  66. eventView: null | EventView;
  67. fetchTransactionData: () => void;
  68. state: ExternalState;
  69. };
  70. const TxnContext = createContext<TxnContextProps>({
  71. eventView: null,
  72. fetchTransactionData: () => {},
  73. state: {didInit: false, errors: [], isFetching: false, traces: []},
  74. });
  75. function ReplayTransactionContext({children, replayRecord}: Options) {
  76. const api = useApi();
  77. const organization = useOrganization();
  78. useEmitTimestampChanges();
  79. const [state, setState] = useState<InternalState>(INITIAL_STATE);
  80. const orgSlug = organization.slug;
  81. const listEventView = useMemo(() => {
  82. if (!replayRecord) {
  83. return null;
  84. }
  85. const replayId = replayRecord?.id;
  86. const projectId = replayRecord?.project_id;
  87. const start = getUtcDateString(replayRecord?.started_at.getTime());
  88. const end = getUtcDateString(replayRecord?.finished_at.getTime());
  89. return EventView.fromSavedQuery({
  90. id: undefined,
  91. name: `Traces in replay ${replayId}`,
  92. fields: ['trace', 'count(trace)', 'min(timestamp)'],
  93. orderby: 'min_timestamp',
  94. query: `replayId:${replayId}`,
  95. projects: [Number(projectId)],
  96. version: 2,
  97. start,
  98. end,
  99. });
  100. }, [replayRecord]);
  101. const fetchSingleTraceData = useCallback(
  102. async dataRow => {
  103. try {
  104. const {trace: traceId, timestamp} = dataRow;
  105. const start = getUtcDateString(replayRecord?.started_at.getTime());
  106. const end = getUtcDateString(replayRecord?.finished_at.getTime());
  107. const eventView = makeEventView({start, end});
  108. let payload;
  109. if (organization.features.includes('replay-trace-view-v1')) {
  110. payload = {
  111. limit: 10000,
  112. useSpans: 1,
  113. timestamp,
  114. };
  115. } else {
  116. payload = getTraceRequestPayload({eventView, location: {} as Location});
  117. }
  118. const [trace, _traceResp] = await doDiscoverQuery<
  119. TraceSplitResults<TraceFullDetailed> | TraceFullDetailed[]
  120. >(api, `/organizations/${orgSlug}/events-trace/${traceId}/`, payload);
  121. const {transactions, orphanErrors} = getTraceSplitResults<TraceFullDetailed>(
  122. trace,
  123. organization
  124. );
  125. setState(prev => {
  126. return {
  127. ...prev,
  128. traces: sortBy(
  129. (prev.traces || []).concat(transactions ?? (trace as TraceFullDetailed[])),
  130. 'start_timestamp'
  131. ),
  132. orphanErrors: sortBy(
  133. (prev.orphanErrors || []).concat(orphanErrors ?? []),
  134. 'timestamp'
  135. ),
  136. };
  137. });
  138. } catch (error) {
  139. setState(prev => ({
  140. ...prev,
  141. detailsErrors: prev.detailsErrors.concat(error),
  142. }));
  143. }
  144. },
  145. [api, orgSlug, organization, replayRecord]
  146. );
  147. const fetchTracesInBatches = useCallback(
  148. async data => {
  149. const clonedData = [...data];
  150. while (clonedData.length > 0) {
  151. const batch = clonedData.splice(0, 3);
  152. // Update state for the current batch request
  153. setState(
  154. prev =>
  155. ({
  156. ...prev,
  157. detailsRequests: prev.detailsRequests + batch.length,
  158. }) as InternalState
  159. );
  160. // Wait for the current batch to finish
  161. await Promise.allSettled(batch.map(fetchSingleTraceData));
  162. // Update state for the current batch response
  163. setState(
  164. prev =>
  165. ({
  166. ...prev,
  167. detailsResponses: prev.detailsResponses + batch.length,
  168. }) as InternalState
  169. );
  170. }
  171. },
  172. [fetchSingleTraceData]
  173. );
  174. const fetchTransactionData = useCallback(async () => {
  175. if (!listEventView) {
  176. return;
  177. }
  178. const start = getUtcDateString(replayRecord?.started_at.getTime());
  179. const end = getUtcDateString(replayRecord?.finished_at.getTime());
  180. setState({
  181. detailsErrors: [],
  182. detailsRequests: 0,
  183. detailsResponses: 0,
  184. didInit: true,
  185. indexComplete: false,
  186. indexError: undefined,
  187. isFetching: true,
  188. traces: [],
  189. });
  190. let cursor = {
  191. cursor: '0:0:0',
  192. results: true,
  193. href: '',
  194. } as ParsedHeader;
  195. while (cursor.results) {
  196. const payload = {
  197. ...listEventView.getEventsAPIPayload({
  198. start,
  199. end,
  200. limit: 10,
  201. } as unknown as Location),
  202. sort: ['min_timestamp', 'trace'],
  203. cursor: cursor.cursor,
  204. };
  205. try {
  206. const [{data}, , listResp] = await doDiscoverQuery<TableData>(
  207. api,
  208. `/organizations/${orgSlug}/events/`,
  209. payload
  210. );
  211. const parsedData = data
  212. .filter(row => row.trace) // Filter out items where trace is not truthy
  213. .map(row => ({
  214. trace: row.trace,
  215. timestamp: getTimeStampFromTableDateField(row['min(timestamp)']),
  216. }));
  217. (async function () {
  218. await fetchTracesInBatches(parsedData);
  219. })();
  220. const pageLinks = listResp?.getResponseHeader('Link') ?? null;
  221. cursor = parseLinkHeader(pageLinks)?.next;
  222. const indexComplete = !cursor.results;
  223. setState(prev => ({...prev, indexComplete}) as InternalState);
  224. } catch (indexError) {
  225. setState(prev => ({...prev, indexError, indexComplete: true}) as InternalState);
  226. cursor = {cursor: '', results: false, href: ''} as ParsedHeader;
  227. }
  228. }
  229. }, [api, listEventView, orgSlug, replayRecord, fetchTracesInBatches]);
  230. const externalState = useMemo(() => internalToExternalState(state), [state]);
  231. return (
  232. <TxnContext.Provider
  233. value={{
  234. eventView: listEventView,
  235. fetchTransactionData,
  236. state: externalState,
  237. }}
  238. >
  239. {children}
  240. </TxnContext.Provider>
  241. );
  242. }
  243. function internalToExternalState({
  244. detailsRequests,
  245. detailsResponses,
  246. didInit,
  247. indexComplete,
  248. indexError,
  249. traces,
  250. orphanErrors,
  251. }: InternalState): ExternalState {
  252. const isComplete = indexComplete && detailsRequests === detailsResponses;
  253. return {
  254. didInit,
  255. errors: indexError ? [indexError] : [], // Ignoring detailsErrors for now
  256. isFetching: !isComplete,
  257. traces,
  258. orphanErrors,
  259. };
  260. }
  261. export default ReplayTransactionContext;
  262. export const useFetchTransactions = () => {
  263. const {fetchTransactionData, state} = useContext(TxnContext);
  264. useEffect(() => {
  265. if (!state.isFetching && state.traces === undefined) {
  266. fetchTransactionData();
  267. }
  268. }, [fetchTransactionData, state]);
  269. };
  270. export const useTransactionData = () => {
  271. const {eventView, state} = useContext(TxnContext);
  272. const data = useMemo(() => ({eventView, state}), [eventView, state]);
  273. return data;
  274. };