useReplaysFromTransaction.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {useCallback, useEffect, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import {Location} from 'history';
  4. import type {Client} from 'sentry/api';
  5. import type {Organization} from 'sentry/types';
  6. import {TableData, TableDataRow} from 'sentry/utils/discover/discoverQuery';
  7. import EventView, {fromSorts} from 'sentry/utils/discover/eventView';
  8. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  9. import {decodeScalar} from 'sentry/utils/queryString';
  10. import fetchReplayList, {
  11. DEFAULT_SORT,
  12. REPLAY_LIST_FIELDS,
  13. } from 'sentry/utils/replays/fetchReplayList';
  14. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  15. import useApi from 'sentry/utils/useApi';
  16. import {ReplayListRecord} from 'sentry/views/replays/types';
  17. export type ReplayListRecordWithTx = ReplayListRecord & {
  18. txEvent: {[x: string]: any};
  19. };
  20. import {SpanOperationBreakdownFilter} from '../filter';
  21. import {
  22. EventsDisplayFilterName,
  23. getEventsFilterOptions,
  24. getPercentilesEventView,
  25. mapPercentileValues,
  26. PercentileValues,
  27. } from '../transactionEvents/utils';
  28. type State = Awaited<ReturnType<typeof fetchReplayList>> & {
  29. eventView: undefined | EventView;
  30. isFetching: boolean;
  31. replays?: ReplayListRecordWithTx[];
  32. };
  33. type Options = {
  34. eventsDisplayFilterName: EventsDisplayFilterName;
  35. eventsWithReplaysView: EventView;
  36. location: Location;
  37. organization: Organization;
  38. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  39. };
  40. function useReplaysFromTransaction({
  41. eventsWithReplaysView,
  42. location,
  43. organization,
  44. spanOperationBreakdownFilter,
  45. eventsDisplayFilterName,
  46. }: Options) {
  47. const api = useApi();
  48. const [data, setData] = useState<State>({
  49. fetchError: undefined,
  50. isFetching: true,
  51. pageLinks: null,
  52. replays: [],
  53. eventView: undefined,
  54. });
  55. const load = useCallback(async () => {
  56. const percentilesView = getPercentilesEventView(eventsWithReplaysView.clone());
  57. const percentileData = await fetchPercentiles({
  58. api,
  59. eventView: percentilesView,
  60. location,
  61. organization,
  62. });
  63. const percentiles = mapPercentileValues(percentileData);
  64. const filteredEventView = getFilteredEventView({
  65. eventView: eventsWithReplaysView,
  66. percentiles,
  67. spanOperationBreakdownFilter,
  68. eventsDisplayFilterName,
  69. });
  70. const eventsData = await fetchEventsWithReplay({
  71. api,
  72. eventView: filteredEventView ?? eventsWithReplaysView,
  73. location,
  74. organization,
  75. });
  76. const replayIds = eventsData?.map(row => row.replayId);
  77. const eventView = EventView.fromNewQueryWithLocation(
  78. {
  79. id: '',
  80. name: 'Replays within a transaction',
  81. version: 2,
  82. fields: REPLAY_LIST_FIELDS,
  83. projects: [],
  84. query: `id:[${String(replayIds)}]`,
  85. },
  86. location
  87. );
  88. eventView.sorts = fromSorts(decodeScalar(location.query.sort, DEFAULT_SORT));
  89. const listData = await fetchReplayList({
  90. api,
  91. eventView,
  92. location,
  93. organization,
  94. });
  95. const replays: ReplayListRecordWithTx[] | undefined = listData.replays?.map(
  96. replay => {
  97. let slowestEvent: TableDataRow | undefined;
  98. for (const event of eventsData ?? []) {
  99. // attach the slowest tx event to the replay
  100. if (
  101. event.replayId === replay.id &&
  102. (!slowestEvent ||
  103. event['transaction.duration'] > slowestEvent['transaction.duration'])
  104. ) {
  105. slowestEvent = event;
  106. }
  107. }
  108. return {
  109. ...replay,
  110. txEvent: slowestEvent ?? {},
  111. };
  112. }
  113. );
  114. setData({
  115. ...listData,
  116. eventView,
  117. isFetching: false,
  118. replays,
  119. });
  120. }, [
  121. api,
  122. eventsWithReplaysView,
  123. location,
  124. organization,
  125. spanOperationBreakdownFilter,
  126. eventsDisplayFilterName,
  127. ]);
  128. useEffect(() => {
  129. load();
  130. }, [load]);
  131. return data;
  132. }
  133. async function fetchEventsWithReplay({
  134. api,
  135. eventView,
  136. location,
  137. organization,
  138. }: {
  139. api: Client;
  140. eventView: EventView;
  141. location: Location;
  142. organization: Organization;
  143. }) {
  144. try {
  145. const [data] = await doDiscoverQuery<TableData>(
  146. api,
  147. `/organizations/${organization.slug}/events/`,
  148. eventView.getEventsAPIPayload(location)
  149. );
  150. return data.data;
  151. } catch (err) {
  152. Sentry.captureException(err);
  153. return null;
  154. }
  155. }
  156. async function fetchPercentiles({
  157. api,
  158. eventView,
  159. location,
  160. organization,
  161. }: {
  162. api: Client;
  163. eventView: EventView;
  164. location: Location;
  165. organization: Organization;
  166. }) {
  167. try {
  168. const [data] = await doDiscoverQuery<TableData>(
  169. api,
  170. `/organizations/${organization.slug}/events/`,
  171. eventView.getEventsAPIPayload(location)
  172. );
  173. return data.data[0];
  174. } catch (err) {
  175. Sentry.captureException(err);
  176. return null;
  177. }
  178. }
  179. function getFilteredEventView({
  180. percentiles,
  181. spanOperationBreakdownFilter,
  182. eventsDisplayFilterName,
  183. eventView,
  184. }: {
  185. eventView: EventView;
  186. eventsDisplayFilterName: EventsDisplayFilterName;
  187. percentiles: PercentileValues;
  188. spanOperationBreakdownFilter: SpanOperationBreakdownFilter;
  189. }) {
  190. const filter = getEventsFilterOptions(spanOperationBreakdownFilter, percentiles)[
  191. eventsDisplayFilterName
  192. ];
  193. const filteredEventView = eventView.clone();
  194. if (filteredEventView && filter?.query) {
  195. const query = new MutableSearch(filteredEventView.query);
  196. filter.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  197. filteredEventView.query = query.formatString();
  198. }
  199. return filteredEventView;
  200. }
  201. export default useReplaysFromTransaction;