traceSearch.tsx 6.9 KB

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