traceSearchEvaluator.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import {waitFor} from 'sentry-test/reactTestingLibrary';
  2. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  3. import type {EventTransaction} from 'sentry/types';
  4. import {
  5. type TraceTree,
  6. TraceTreeNode,
  7. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  8. import {searchInTraceTreeTokens} from 'sentry/views/performance/newTraceDetails/traceSearch/traceSearchEvaluator';
  9. import {parseTraceSearch} from 'sentry/views/performance/newTraceDetails/traceSearch/traceTokenConverter';
  10. function makeTransaction(
  11. overrides: Partial<TraceTree.Transaction> = {}
  12. ): TraceTree.Transaction {
  13. return {
  14. children: [],
  15. start_timestamp: 0,
  16. timestamp: 1,
  17. transaction: 'transaction',
  18. 'transaction.op': '',
  19. 'transaction.status': '',
  20. performance_issues: [],
  21. errors: [],
  22. ...overrides,
  23. } as TraceTree.Transaction;
  24. }
  25. function makeSpan(overrides: Partial<RawSpanType> = {}): TraceTree.Span {
  26. return {
  27. span_id: '',
  28. op: '',
  29. description: '',
  30. start_timestamp: 0,
  31. timestamp: 10,
  32. data: {},
  33. trace_id: '',
  34. childTransactions: [],
  35. event: undefined as unknown as EventTransaction,
  36. ...overrides,
  37. };
  38. }
  39. const makeTree = (list: TraceTree.NodeValue[]): TraceTree => {
  40. return {
  41. list: list.map(
  42. n => new TraceTreeNode(null, n, {project_slug: 'project', event_id: ''})
  43. ),
  44. } as unknown as TraceTree;
  45. };
  46. const search = (query: string, tree: TraceTree, cb: any) => {
  47. searchInTraceTreeTokens(
  48. tree,
  49. // @ts-expect-error dont care if this fails
  50. parseTraceSearch(query),
  51. null,
  52. cb
  53. );
  54. };
  55. describe('TraceSearchEvaluator', () => {
  56. it('empty string', async () => {
  57. const list = makeTree([
  58. makeTransaction({'transaction.op': 'operation'}),
  59. makeTransaction({'transaction.op': 'other'}),
  60. ]);
  61. const cb = jest.fn();
  62. search('', list, cb);
  63. await waitFor(() => {
  64. expect(cb).toHaveBeenCalled();
  65. });
  66. expect(cb.mock.calls[0][0][0]).toEqual([]);
  67. expect(cb.mock.calls[0][0][1].size).toBe(0);
  68. expect(cb.mock.calls[0][0][2]).toBe(null);
  69. });
  70. it.each([
  71. [''],
  72. ['invalid_query'],
  73. ['invalid_query:'],
  74. ['OR'],
  75. ['AND'],
  76. ['('],
  77. [')'],
  78. ['()'],
  79. ['(invalid_query)'],
  80. ])('invalid grammar %s', async query => {
  81. const list = makeTree([
  82. makeTransaction({'transaction.op': 'operation'}),
  83. makeTransaction({'transaction.op': 'other'}),
  84. ]);
  85. const cb = jest.fn();
  86. search(query, list, cb);
  87. await waitFor(() => {
  88. expect(cb).toHaveBeenCalled();
  89. });
  90. expect(cb.mock.calls[0][0][0]).toEqual([]);
  91. expect(cb.mock.calls[0][0][1].size).toBe(0);
  92. expect(cb.mock.calls[0][0][2]).toBe(null);
  93. });
  94. it('AND query', async () => {
  95. const tree = makeTree([
  96. makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
  97. makeTransaction({'transaction.op': 'other'}),
  98. ]);
  99. const cb = jest.fn();
  100. search('transaction.op:operation AND transaction:something', tree, cb);
  101. await waitFor(() => {
  102. expect(cb).toHaveBeenCalled();
  103. });
  104. expect(cb.mock.calls[0][0][1].size).toBe(1);
  105. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  106. expect(cb.mock.calls[0][0][2]).toBe(null);
  107. });
  108. it('OR query', async () => {
  109. const tree = makeTree([
  110. makeTransaction({'transaction.op': 'operation'}),
  111. makeTransaction({'transaction.op': 'other'}),
  112. ]);
  113. const cb = jest.fn();
  114. search('transaction.op:operation OR transaction.op:other', tree, cb);
  115. await waitFor(() => {
  116. expect(cb).toHaveBeenCalled();
  117. });
  118. expect(cb.mock.calls[0][0][0]).toEqual([
  119. {index: 0, value: tree.list[0]},
  120. {index: 1, value: tree.list[1]},
  121. ]);
  122. expect(cb.mock.calls[0][0][1].size).toBe(2);
  123. expect(cb.mock.calls[0][0][2]).toBe(null);
  124. });
  125. it('OR with AND respects precedence', async () => {
  126. const tree = makeTree([
  127. makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
  128. makeTransaction({'transaction.op': 'other', transaction: ''}),
  129. ]);
  130. const cb = jest.fn();
  131. // @TODO check if this makes sense with some users. We might only want to do this only if we have a set of parens.
  132. search(
  133. // (transaction.op:operation OR transaction.op:other) AND transaction:something
  134. 'transaction.op:operation AND transaction:something OR transaction.op:other',
  135. tree,
  136. cb
  137. );
  138. await waitFor(() => {
  139. expect(cb).toHaveBeenCalled();
  140. });
  141. expect(cb.mock.calls[0][0][1].size).toBe(1);
  142. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  143. expect(cb.mock.calls[0][0][2]).toBe(null);
  144. });
  145. describe('transaction', () => {
  146. it('text filter', async () => {
  147. const tree = makeTree([
  148. makeTransaction({'transaction.op': 'operation'}),
  149. makeTransaction({'transaction.op': 'other'}),
  150. ]);
  151. const cb = jest.fn();
  152. search('transaction.op:operation', tree, cb);
  153. await waitFor(() => expect(cb).toHaveBeenCalled());
  154. expect(cb.mock.calls[0][0][1].size).toBe(1);
  155. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  156. expect(cb.mock.calls[0][0][2]).toBe(null);
  157. });
  158. it('text filter with prefix', async () => {
  159. const tree = makeTree([makeTransaction({transaction: 'operation'})]);
  160. const cb = jest.fn();
  161. search('transaction.transaction:operation', tree, cb);
  162. await waitFor(() => expect(cb).toHaveBeenCalled());
  163. expect(cb.mock.calls[0][0][1].size).toBe(1);
  164. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  165. expect(cb.mock.calls[0][0][2]).toBe(null);
  166. });
  167. it('transaction.duration (milliseconds)', async () => {
  168. const tree = makeTree([
  169. makeTransaction({'transaction.duration': 1000}),
  170. makeTransaction({'transaction.duration': 500}),
  171. ]);
  172. const cb = jest.fn();
  173. search('transaction.duration:>500ms', tree, cb);
  174. await waitFor(() => expect(cb).toHaveBeenCalled());
  175. expect(cb.mock.calls[0][0][1].size).toBe(1);
  176. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  177. expect(cb.mock.calls[0][0][2]).toBe(null);
  178. });
  179. it('transaction.duration (seconds)', async () => {
  180. const tree = makeTree([
  181. makeTransaction({'transaction.duration': 1000}),
  182. makeTransaction({'transaction.duration': 500}),
  183. ]);
  184. const cb = jest.fn();
  185. search('transaction.duration:>0.5s', tree, cb);
  186. await waitFor(() => expect(cb).toHaveBeenCalled());
  187. expect(cb.mock.calls[0][0][1].size).toBe(1);
  188. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  189. expect(cb.mock.calls[0][0][2]).toBe(null);
  190. });
  191. it('transaction.total_time', async () => {
  192. const tree = makeTree([
  193. makeTransaction({start_timestamp: 0, timestamp: 1}),
  194. makeTransaction({start_timestamp: 0, timestamp: 0.5}),
  195. ]);
  196. const cb = jest.fn();
  197. search('transaction.total_time:>0.5s', tree, cb);
  198. await waitFor(() => expect(cb).toHaveBeenCalled());
  199. expect(cb.mock.calls[0][0][1].size).toBe(1);
  200. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  201. expect(cb.mock.calls[0][0][2]).toBe(null);
  202. });
  203. // For consistency between spans and txns, should should be implemented
  204. // it('transaction.self_time', () => {});
  205. });
  206. describe('span', () => {
  207. it('text filter', async () => {
  208. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  209. const cb = jest.fn();
  210. search('op:db', tree, cb);
  211. await waitFor(() => expect(cb).toHaveBeenCalled());
  212. expect(cb.mock.calls[0][0][1].size).toBe(1);
  213. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  214. expect(cb.mock.calls[0][0][2]).toBe(null);
  215. });
  216. it('text filter with prefix', async () => {
  217. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  218. const cb = jest.fn();
  219. search('span.op:db', tree, cb);
  220. await waitFor(() => expect(cb).toHaveBeenCalled());
  221. expect(cb.mock.calls[0][0][1].size).toBe(1);
  222. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  223. expect(cb.mock.calls[0][0][2]).toBe(null);
  224. });
  225. it('span.duration (milliseconds)', async () => {
  226. const tree = makeTree([
  227. makeSpan({start_timestamp: 0, timestamp: 1}),
  228. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  229. ]);
  230. const cb = jest.fn();
  231. search('span.duration:>500ms', tree, cb);
  232. await waitFor(() => expect(cb).toHaveBeenCalled());
  233. expect(cb.mock.calls[0][0][1].size).toBe(1);
  234. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  235. expect(cb.mock.calls[0][0][2]).toBe(null);
  236. });
  237. it('span.duration (seconds)', async () => {
  238. const tree = makeTree([
  239. makeSpan({start_timestamp: 0, timestamp: 1}),
  240. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  241. ]);
  242. const cb = jest.fn();
  243. search('span.duration:>0.5s', tree, cb);
  244. await waitFor(() => expect(cb).toHaveBeenCalled());
  245. expect(cb.mock.calls[0][0][1].size).toBe(1);
  246. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  247. expect(cb.mock.calls[0][0][2]).toBe(null);
  248. });
  249. it('span.total_time', async () => {
  250. const tree = makeTree([
  251. makeSpan({start_timestamp: 0, timestamp: 1}),
  252. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  253. ]);
  254. const cb = jest.fn();
  255. search('span.total_time:>0.5s', tree, cb);
  256. await waitFor(() => expect(cb).toHaveBeenCalled());
  257. expect(cb.mock.calls[0][0][1].size).toBe(1);
  258. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  259. expect(cb.mock.calls[0][0][2]).toBe(null);
  260. });
  261. it('span.self_time', async () => {
  262. const tree = makeTree([
  263. makeSpan({exclusive_time: 1000}),
  264. makeSpan({exclusive_time: 500}),
  265. ]);
  266. const cb = jest.fn();
  267. search('span.self_time:>0.5s', tree, cb);
  268. await waitFor(() => expect(cb).toHaveBeenCalled());
  269. expect(cb.mock.calls[0][0][1].size).toBe(1);
  270. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  271. expect(cb.mock.calls[0][0][2]).toBe(null);
  272. });
  273. it('span.exclusive_time', async () => {
  274. const tree = makeTree([
  275. makeSpan({exclusive_time: 1000}),
  276. makeSpan({exclusive_time: 500}),
  277. ]);
  278. const cb = jest.fn();
  279. search('span.exclusive_time:>0.5s', tree, cb);
  280. await waitFor(() => expect(cb).toHaveBeenCalled());
  281. expect(cb.mock.calls[0][0][1].size).toBe(1);
  282. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  283. expect(cb.mock.calls[0][0][2]).toBe(null);
  284. });
  285. it('exclusive_time', async () => {
  286. const tree = makeTree([
  287. makeSpan({exclusive_time: 1000}),
  288. makeSpan({exclusive_time: 500}),
  289. ]);
  290. const cb = jest.fn();
  291. search('exclusive_time:>0.5s', tree, cb);
  292. await waitFor(() => expect(cb).toHaveBeenCalled());
  293. expect(cb.mock.calls[0][0][1].size).toBe(1);
  294. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  295. expect(cb.mock.calls[0][0][2]).toBe(null);
  296. });
  297. });
  298. });