traceSearch.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import {
  2. isSpanNode,
  3. isTraceErrorNode,
  4. isTransactionNode,
  5. } from 'sentry/views/performance/newTraceDetails/guards';
  6. import type {
  7. TraceTree,
  8. TraceTreeNode,
  9. } from 'sentry/views/performance/newTraceDetails/traceTree';
  10. export type TraceSearchAction =
  11. | {query: string | undefined; type: 'set query'}
  12. | {type: 'go to next match'}
  13. | {type: 'go to previous match'}
  14. | {resultIndex: number; resultIteratorIndex: number; type: 'set iterator index'}
  15. | {type: 'clear iterator index'}
  16. | {type: 'clear query'}
  17. | {
  18. results: ReadonlyArray<TraceResult>;
  19. resultsLookup: Map<TraceTreeNode<TraceTree.NodeValue>, number>;
  20. type: 'set results';
  21. };
  22. export type TraceSearchState = {
  23. query: string | undefined;
  24. resultIndex: number | undefined;
  25. resultIteratorIndex: number | undefined;
  26. results: ReadonlyArray<TraceResult> | undefined;
  27. resultsLookup: Map<TraceTreeNode<TraceTree.NodeValue>, number>;
  28. status: [ts: number, 'loading' | 'success' | 'error'] | undefined;
  29. };
  30. function assertBoundedIndex(index: number, length: number) {
  31. if (index < 0 || index > length - 1) {
  32. throw new Error('Search index out of bounds');
  33. }
  34. }
  35. export function traceSearchReducer(
  36. state: TraceSearchState,
  37. action: TraceSearchAction
  38. ): TraceSearchState {
  39. switch (action.type) {
  40. case 'clear query': {
  41. return {
  42. query: undefined,
  43. resultIteratorIndex: undefined,
  44. results: undefined,
  45. resultIndex: undefined,
  46. resultsLookup: new Map(),
  47. status: undefined,
  48. };
  49. }
  50. case 'go to next match': {
  51. if (state.resultIteratorIndex === undefined) {
  52. if (!state.results || state.results.length === 0) {
  53. return state;
  54. }
  55. return {...state, resultIteratorIndex: 0, resultIndex: state.results[0].index};
  56. }
  57. if (!state.results) return state;
  58. let next = state.resultIteratorIndex + 1;
  59. if (next > state.results.length - 1) {
  60. next = 0;
  61. }
  62. assertBoundedIndex(next, state.results.length);
  63. return {
  64. ...state,
  65. resultIteratorIndex: next,
  66. resultIndex: state.results[next].index,
  67. };
  68. }
  69. case 'go to previous match': {
  70. if (state.resultIteratorIndex === undefined) {
  71. if (!state.results || !state.results.length) {
  72. return state;
  73. }
  74. return {
  75. ...state,
  76. resultIteratorIndex: state.results.length - 1,
  77. resultIndex: state.results[state.results.length - 1].index,
  78. };
  79. }
  80. if (!state.results) return state;
  81. let previous = state.resultIteratorIndex - 1;
  82. if (previous < 0) {
  83. previous = state.results.length - 1;
  84. }
  85. assertBoundedIndex(previous, state.results.length);
  86. return {
  87. ...state,
  88. resultIteratorIndex: previous,
  89. resultIndex: state.results[previous].index,
  90. };
  91. }
  92. case 'set results': {
  93. return {
  94. ...state,
  95. status: [performance.now(), 'success'],
  96. results: action.results,
  97. resultIteratorIndex: undefined,
  98. resultIndex: undefined,
  99. resultsLookup: action.resultsLookup,
  100. };
  101. }
  102. case 'set query': {
  103. return {
  104. ...state,
  105. status: [performance.now(), 'loading'],
  106. query: action.query,
  107. resultIteratorIndex: undefined,
  108. resultIndex: undefined,
  109. resultsLookup: new Map(),
  110. };
  111. }
  112. case 'set iterator index': {
  113. return {
  114. ...state,
  115. resultIteratorIndex: action.resultIteratorIndex,
  116. resultIndex: action.resultIndex,
  117. };
  118. }
  119. case 'clear iterator index': {
  120. return {...state, resultIteratorIndex: undefined, resultIndex: undefined};
  121. }
  122. default: {
  123. throw new Error('Invalid trace search reducer action');
  124. }
  125. }
  126. }
  127. type TraceResult = {
  128. index: number;
  129. value: TraceTreeNode<TraceTree.NodeValue>;
  130. };
  131. export function searchInTraceTree(
  132. query: string,
  133. tree: TraceTree,
  134. cb: (
  135. results: [ReadonlyArray<TraceResult>, Map<TraceTreeNode<TraceTree.NodeValue>, number>]
  136. ) => void
  137. ): {id: number | null} {
  138. const raf: {id: number | null} = {id: 0};
  139. const results: Array<TraceResult> = [];
  140. const resultLookup = new Map();
  141. let i = 0;
  142. let matchCount = 0;
  143. const count = tree.list.length;
  144. function search() {
  145. const ts = performance.now();
  146. while (i < count && performance.now() - ts < 12) {
  147. const node = tree.list[i];
  148. if (searchInTraceSubset(query, node)) {
  149. results.push({index: i, value: node});
  150. resultLookup.set(node, matchCount);
  151. matchCount++;
  152. }
  153. i++;
  154. }
  155. if (i < count) {
  156. raf.id = requestAnimationFrame(search);
  157. }
  158. if (i === count) {
  159. cb([results, resultLookup]);
  160. raf.id = null;
  161. }
  162. }
  163. raf.id = requestAnimationFrame(search);
  164. return raf;
  165. }
  166. function searchInTraceSubset(
  167. query: string,
  168. node: TraceTreeNode<TraceTree.NodeValue>
  169. ): boolean {
  170. if (isSpanNode(node)) {
  171. if (node.value.op?.includes(query)) {
  172. return true;
  173. }
  174. if (node.value.description?.includes(query)) {
  175. return true;
  176. }
  177. if (node.value.span_id && node.value.span_id === query) {
  178. return true;
  179. }
  180. }
  181. if (isTransactionNode(node)) {
  182. if (node.value['transaction.op']?.includes(query)) {
  183. return true;
  184. }
  185. if (node.value.transaction?.includes(query)) {
  186. return true;
  187. }
  188. if (node.value.event_id && node.value.event_id === query) {
  189. return true;
  190. }
  191. }
  192. if (isTraceErrorNode(node)) {
  193. if (node.value.level === query) {
  194. return true;
  195. }
  196. if (node.value.title?.includes(query)) {
  197. return true;
  198. }
  199. }
  200. return false;
  201. }