traceSearch.tsx 6.8 KB

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