traceSearch.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import {
  2. isAutogroupedNode,
  3. isSpanNode,
  4. isTraceErrorNode,
  5. isTransactionNode,
  6. } from 'sentry/views/performance/newTraceDetails/guards';
  7. import type {
  8. TraceTree,
  9. TraceTreeNode,
  10. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  11. import {traceReducerExhaustiveActionCheck} from 'sentry/views/performance/newTraceDetails/traceState';
  12. export type TraceSearchAction =
  13. | {query: string | undefined; type: 'set query'}
  14. | {type: 'go to first match'}
  15. | {type: 'go to last match'}
  16. | {type: 'go to next match'}
  17. | {type: 'go to previous match'}
  18. | {
  19. resultIndex: number;
  20. resultIteratorIndex: number;
  21. type: 'set search iterator index';
  22. }
  23. | {type: 'clear'}
  24. | {type: 'clear search iterator index'}
  25. | {type: 'clear query'}
  26. | {
  27. node: TraceTreeNode<TraceTree.NodeValue> | null;
  28. previousNode: {
  29. resultIndex: number | undefined;
  30. resultIteratorIndex: number | undefined;
  31. } | null;
  32. results: ReadonlyArray<TraceResult>;
  33. resultsLookup: Map<TraceTreeNode<TraceTree.NodeValue>, number>;
  34. type: 'set results';
  35. resultIndex?: number;
  36. resultIteratorIndex?: number;
  37. };
  38. export type TraceSearchState = {
  39. node: TraceTreeNode<TraceTree.NodeValue> | null;
  40. query: string | undefined;
  41. // Index in the list/tree
  42. resultIndex: number | null;
  43. // Index in the results array
  44. resultIteratorIndex: number | null;
  45. results: ReadonlyArray<TraceResult> | null;
  46. resultsLookup: Map<TraceTreeNode<TraceTree.NodeValue>, number>;
  47. status: [ts: number, 'loading' | 'success' | 'error'] | undefined;
  48. };
  49. function assertBoundedIndex(index: number, length: number) {
  50. if (index < 0 || index > length - 1) {
  51. throw new Error('Search index out of bounds');
  52. }
  53. }
  54. export function traceSearchReducer(
  55. state: TraceSearchState,
  56. action: TraceSearchAction
  57. ): TraceSearchState {
  58. switch (action.type) {
  59. case 'clear query': {
  60. return {
  61. node: null,
  62. query: undefined,
  63. resultIteratorIndex: null,
  64. results: null,
  65. resultIndex: null,
  66. resultsLookup: new Map(),
  67. status: undefined,
  68. };
  69. }
  70. case 'go to first match': {
  71. if (!state.results || state.results.length === 0) {
  72. return state;
  73. }
  74. return {
  75. ...state,
  76. node: state.results[0].value,
  77. resultIteratorIndex: 0,
  78. resultIndex: state.results[0].index,
  79. };
  80. }
  81. case 'go to last match': {
  82. if (!state.results || state.results.length === 0) {
  83. return state;
  84. }
  85. return {
  86. ...state,
  87. resultIteratorIndex: state.results.length - 1,
  88. resultIndex: state.results[state.results.length - 1].index,
  89. node: state.results[state.results.length - 1].value,
  90. };
  91. }
  92. case 'go to next match': {
  93. if (state.resultIteratorIndex === null) {
  94. if (!state.results || state.results.length === 0) {
  95. return state;
  96. }
  97. return {
  98. ...state,
  99. resultIteratorIndex: 0,
  100. resultIndex: state.results[0].index,
  101. node: state.results[0].value,
  102. };
  103. }
  104. if (!state.results) return state;
  105. let next = state.resultIteratorIndex + 1;
  106. if (next > state.results.length - 1) {
  107. next = 0;
  108. }
  109. assertBoundedIndex(next, state.results.length);
  110. return {
  111. ...state,
  112. resultIteratorIndex: next,
  113. resultIndex: state.results[next].index,
  114. node: state.results[next].value,
  115. };
  116. }
  117. case 'go to previous match': {
  118. if (state.resultIteratorIndex === null) {
  119. if (!state.results || !state.results.length) {
  120. return state;
  121. }
  122. return {
  123. ...state,
  124. resultIteratorIndex: state.results.length - 1,
  125. resultIndex: state.results[state.results.length - 1].index,
  126. node: state.results[state.results.length - 1].value,
  127. };
  128. }
  129. if (!state.results) return state;
  130. let previous = state.resultIteratorIndex - 1;
  131. if (previous < 0) {
  132. previous = state.results.length - 1;
  133. }
  134. assertBoundedIndex(previous, state.results.length);
  135. return {
  136. ...state,
  137. resultIteratorIndex: previous,
  138. resultIndex: state.results[previous].index,
  139. node: state.results[previous].value,
  140. };
  141. }
  142. case 'set results': {
  143. return {
  144. ...state,
  145. status: [performance.now(), 'success'],
  146. results: action.results,
  147. resultIteratorIndex: action.resultIteratorIndex ?? null,
  148. node: action.node ?? null,
  149. resultIndex: action.resultIndex ?? null,
  150. resultsLookup: action.resultsLookup,
  151. };
  152. }
  153. case 'set query': {
  154. return {
  155. ...state,
  156. status: [performance.now(), 'loading'],
  157. query: action.query,
  158. };
  159. }
  160. case 'set search iterator index': {
  161. return {
  162. ...state,
  163. node: state.results?.[action.resultIteratorIndex]?.value ?? null,
  164. resultIteratorIndex: action.resultIteratorIndex,
  165. resultIndex: action.resultIndex,
  166. };
  167. }
  168. case 'clear search iterator index':
  169. return {
  170. ...state,
  171. resultIteratorIndex: null,
  172. resultIndex: null,
  173. node: null,
  174. };
  175. case 'clear': {
  176. return {...state, node: null, resultIteratorIndex: null, resultIndex: null};
  177. }
  178. default: {
  179. traceReducerExhaustiveActionCheck(action);
  180. return state;
  181. }
  182. }
  183. }
  184. type TraceResult = {
  185. index: number;
  186. value: TraceTreeNode<TraceTree.NodeValue>;
  187. };
  188. export function searchInTraceTree(
  189. tree: TraceTree,
  190. query: string,
  191. previousNode: TraceTreeNode<TraceTree.NodeValue> | null,
  192. cb: (
  193. results: [
  194. ReadonlyArray<TraceResult>,
  195. Map<TraceTreeNode<TraceTree.NodeValue>, number>,
  196. {resultIndex: number | undefined; resultIteratorIndex: number | undefined} | null,
  197. ]
  198. ) => void
  199. ): {id: number | null} {
  200. const raf: {id: number | null} = {id: 0};
  201. let previousNodeSearchResult: {
  202. resultIndex: number | undefined;
  203. resultIteratorIndex: number | undefined;
  204. } | null = null;
  205. const results: Array<TraceResult> = [];
  206. const resultLookup = new Map();
  207. let i = 0;
  208. let matchCount = 0;
  209. const count = tree.list.length;
  210. function search() {
  211. const ts = performance.now();
  212. while (i < count && performance.now() - ts < 12) {
  213. const node = tree.list[i];
  214. if (searchInTraceSubset(query, node)) {
  215. results.push({index: i, value: node});
  216. resultLookup.set(node, matchCount);
  217. if (previousNode === node) {
  218. previousNodeSearchResult = {
  219. resultIndex: i,
  220. resultIteratorIndex: matchCount,
  221. };
  222. }
  223. matchCount++;
  224. }
  225. i++;
  226. }
  227. if (i < count) {
  228. raf.id = requestAnimationFrame(search);
  229. }
  230. if (i === count) {
  231. cb([results, resultLookup, previousNodeSearchResult]);
  232. raf.id = null;
  233. }
  234. }
  235. raf.id = requestAnimationFrame(search);
  236. return raf;
  237. }
  238. function searchInTraceSubset(
  239. query: string,
  240. node: TraceTreeNode<TraceTree.NodeValue>
  241. ): boolean {
  242. if (isSpanNode(node)) {
  243. if (node.value.op?.includes(query)) {
  244. return true;
  245. }
  246. if (node.value.description?.includes(query)) {
  247. return true;
  248. }
  249. if (node.value.span_id && node.value.span_id === query) {
  250. return true;
  251. }
  252. }
  253. if (isTransactionNode(node)) {
  254. if (node.value['transaction.op']?.includes(query)) {
  255. return true;
  256. }
  257. if (node.value.transaction?.includes(query)) {
  258. return true;
  259. }
  260. if (node.value.event_id && node.value.event_id === query) {
  261. return true;
  262. }
  263. }
  264. if (isAutogroupedNode(node)) {
  265. if (node.value.op?.includes(query)) {
  266. return true;
  267. }
  268. if (node.value.description?.includes(query)) {
  269. return true;
  270. }
  271. }
  272. if (isTraceErrorNode(node)) {
  273. if (node.value.level === query) {
  274. return true;
  275. }
  276. if (node.value.title?.includes(query)) {
  277. return true;
  278. }
  279. }
  280. return false;
  281. }