traceSearchEvaluator.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import {waitFor} from 'sentry-test/reactTestingLibrary';
  2. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  3. import type {TraceTree} from '../traceModels/traceTree';
  4. import {TraceTreeNode} from '../traceModels/traceTreeNode';
  5. import {searchInTraceTreeTokens} from '../traceSearch/traceSearchEvaluator';
  6. import {parseTraceSearch} from '../traceSearch/traceTokenConverter';
  7. function makeTransaction(
  8. overrides: Partial<TraceTree.Transaction> = {}
  9. ): TraceTree.Transaction {
  10. return {
  11. children: [],
  12. start_timestamp: 0,
  13. timestamp: 1,
  14. transaction: 'transaction',
  15. 'transaction.op': '',
  16. 'transaction.status': '',
  17. performance_issues: [],
  18. errors: [],
  19. ...overrides,
  20. } as TraceTree.Transaction;
  21. }
  22. function makeSpan(overrides: Partial<RawSpanType> = {}): TraceTree.Span {
  23. return {
  24. span_id: '',
  25. op: '',
  26. description: '',
  27. start_timestamp: 0,
  28. timestamp: 10,
  29. data: {},
  30. trace_id: '',
  31. ...overrides,
  32. };
  33. }
  34. function makeError(overrides: Partial<TraceTree.TraceError> = {}): TraceTree.TraceError {
  35. return {
  36. issue_id: 1,
  37. issue: 'dead issue',
  38. event_id: 'event_id',
  39. project_slug: 'project',
  40. project_id: 1,
  41. level: 'fatal',
  42. title: 'dead',
  43. message: 'dead message',
  44. span: '1',
  45. ...overrides,
  46. };
  47. }
  48. function makePerformanceIssue(
  49. overrides: Partial<TraceTree.TracePerformanceIssue> = {}
  50. ): TraceTree.TracePerformanceIssue {
  51. return {
  52. event_id: 'event_id',
  53. project_slug: 'project',
  54. message: 'dead message',
  55. title: 'dead',
  56. issue_id: 1,
  57. level: 'fatal',
  58. project_id: 1,
  59. culprit: 'culprit',
  60. start: 0,
  61. end: 1,
  62. span: [],
  63. suspect_spans: [],
  64. type: 0,
  65. ...overrides,
  66. };
  67. }
  68. const makeTree = (list: TraceTree.NodeValue[]): TraceTree => {
  69. return {
  70. list: list.map(
  71. n => new TraceTreeNode(null, n, {project_slug: 'project', event_id: ''})
  72. ),
  73. } as unknown as TraceTree;
  74. };
  75. const search = (query: string, tree: TraceTree, cb: any) => {
  76. searchInTraceTreeTokens(
  77. tree,
  78. // @ts-expect-error dont care if this fails
  79. parseTraceSearch(query),
  80. null,
  81. cb
  82. );
  83. };
  84. describe('TraceSearchEvaluator', () => {
  85. it('empty string', async () => {
  86. const list = makeTree([
  87. makeTransaction({'transaction.op': 'operation'}),
  88. makeTransaction({'transaction.op': 'other'}),
  89. ]);
  90. const cb = jest.fn();
  91. search('', list, cb);
  92. await waitFor(() => {
  93. expect(cb).toHaveBeenCalled();
  94. });
  95. expect(cb.mock.calls[0][0][0]).toEqual([]);
  96. expect(cb.mock.calls[0][0][1].size).toBe(0);
  97. expect(cb.mock.calls[0][0][2]).toBe(null);
  98. });
  99. it.each([
  100. [''],
  101. ['invalid_query'],
  102. ['invalid_query:'],
  103. ['OR'],
  104. ['AND'],
  105. ['('],
  106. [')'],
  107. ['()'],
  108. ['(invalid_query)'],
  109. ])('invalid grammar %s', async query => {
  110. const list = makeTree([
  111. makeTransaction({'transaction.op': 'operation'}),
  112. makeTransaction({'transaction.op': 'other'}),
  113. ]);
  114. const cb = jest.fn();
  115. search(query, list, cb);
  116. await waitFor(() => {
  117. expect(cb).toHaveBeenCalled();
  118. });
  119. expect(cb.mock.calls[0][0][0]).toEqual([]);
  120. expect(cb.mock.calls[0][0][1].size).toBe(0);
  121. expect(cb.mock.calls[0][0][2]).toBe(null);
  122. });
  123. it('AND query', async () => {
  124. const tree = makeTree([
  125. makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
  126. makeTransaction({'transaction.op': 'other'}),
  127. ]);
  128. const cb = jest.fn();
  129. search('transaction.op:operation AND transaction:something', tree, cb);
  130. await waitFor(() => {
  131. expect(cb).toHaveBeenCalled();
  132. });
  133. expect(cb.mock.calls[0][0][1].size).toBe(1);
  134. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  135. expect(cb.mock.calls[0][0][2]).toBe(null);
  136. });
  137. it('OR query', async () => {
  138. const tree = makeTree([
  139. makeTransaction({'transaction.op': 'operation'}),
  140. makeTransaction({'transaction.op': 'other'}),
  141. ]);
  142. const cb = jest.fn();
  143. search('transaction.op:operation OR transaction.op:other', tree, cb);
  144. await waitFor(() => {
  145. expect(cb).toHaveBeenCalled();
  146. });
  147. expect(cb.mock.calls[0][0][0]).toEqual([
  148. {index: 0, value: tree.list[0]},
  149. {index: 1, value: tree.list[1]},
  150. ]);
  151. expect(cb.mock.calls[0][0][1].size).toBe(2);
  152. expect(cb.mock.calls[0][0][2]).toBe(null);
  153. });
  154. it('OR with AND respects precedence', async () => {
  155. const tree = makeTree([
  156. makeTransaction({'transaction.op': 'operation', transaction: 'something'}),
  157. makeTransaction({'transaction.op': 'other', transaction: ''}),
  158. ]);
  159. const cb = jest.fn();
  160. search(
  161. 'transaction.op:operation AND transaction:something OR transaction.op:other',
  162. tree,
  163. cb
  164. );
  165. await waitFor(() => {
  166. expect(cb).toHaveBeenCalled();
  167. });
  168. expect(cb.mock.calls[0][0][1].size).toBe(2);
  169. expect(cb.mock.calls[0][0][0]).toEqual([
  170. {index: 0, value: tree.list[0]},
  171. {index: 1, value: tree.list[1]},
  172. ]);
  173. expect(cb.mock.calls[0][0][2]).toBe(null);
  174. });
  175. describe('transaction', () => {
  176. it('text filter', async () => {
  177. const tree = makeTree([
  178. makeTransaction({'transaction.op': 'operation'}),
  179. makeTransaction({'transaction.op': 'other'}),
  180. ]);
  181. const cb = jest.fn();
  182. search('transaction.op:operation', tree, cb);
  183. await waitFor(() => expect(cb).toHaveBeenCalled());
  184. expect(cb.mock.calls[0][0][1].size).toBe(1);
  185. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  186. expect(cb.mock.calls[0][0][2]).toBe(null);
  187. });
  188. it('text filter with prefix', async () => {
  189. const tree = makeTree([makeTransaction({transaction: 'operation'})]);
  190. const cb = jest.fn();
  191. search('transaction.transaction:operation', tree, cb);
  192. await waitFor(() => expect(cb).toHaveBeenCalled());
  193. expect(cb.mock.calls[0][0][1].size).toBe(1);
  194. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  195. expect(cb.mock.calls[0][0][2]).toBe(null);
  196. });
  197. it('transaction.duration (milliseconds)', async () => {
  198. const tree = makeTree([
  199. makeTransaction({'transaction.duration': 1000}),
  200. makeTransaction({'transaction.duration': 500}),
  201. ]);
  202. const cb = jest.fn();
  203. search('transaction.duration:>500ms', tree, cb);
  204. await waitFor(() => expect(cb).toHaveBeenCalled());
  205. expect(cb.mock.calls[0][0][1].size).toBe(1);
  206. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  207. expect(cb.mock.calls[0][0][2]).toBe(null);
  208. });
  209. it('transaction.duration (seconds)', async () => {
  210. const tree = makeTree([
  211. makeTransaction({'transaction.duration': 1000}),
  212. makeTransaction({'transaction.duration': 500}),
  213. ]);
  214. const cb = jest.fn();
  215. search('transaction.duration:>0.5s', tree, cb);
  216. await waitFor(() => expect(cb).toHaveBeenCalled());
  217. expect(cb.mock.calls[0][0][1].size).toBe(1);
  218. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  219. expect(cb.mock.calls[0][0][2]).toBe(null);
  220. });
  221. it('transaction.total_time', async () => {
  222. const tree = makeTree([
  223. makeTransaction({start_timestamp: 0, timestamp: 1}),
  224. makeTransaction({start_timestamp: 0, timestamp: 0.5}),
  225. ]);
  226. const cb = jest.fn();
  227. search('transaction.total_time:>0.5s', tree, cb);
  228. await waitFor(() => expect(cb).toHaveBeenCalled());
  229. expect(cb.mock.calls[0][0][1].size).toBe(1);
  230. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  231. expect(cb.mock.calls[0][0][2]).toBe(null);
  232. });
  233. // For consistency between spans and txns, should should be implemented
  234. // it('transaction.self_time', () => {});
  235. });
  236. describe('span', () => {
  237. it('text filter', async () => {
  238. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  239. const cb = jest.fn();
  240. search('op:db', tree, cb);
  241. await waitFor(() => expect(cb).toHaveBeenCalled());
  242. expect(cb.mock.calls[0][0][1].size).toBe(1);
  243. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  244. expect(cb.mock.calls[0][0][2]).toBe(null);
  245. });
  246. it('text filter with prefix', async () => {
  247. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  248. const cb = jest.fn();
  249. search('span.op:db', tree, cb);
  250. await waitFor(() => expect(cb).toHaveBeenCalled());
  251. expect(cb.mock.calls[0][0][1].size).toBe(1);
  252. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  253. expect(cb.mock.calls[0][0][2]).toBe(null);
  254. });
  255. it('span.duration (milliseconds)', async () => {
  256. const tree = makeTree([
  257. makeSpan({start_timestamp: 0, timestamp: 1}),
  258. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  259. ]);
  260. const cb = jest.fn();
  261. search('span.duration:>500ms', tree, cb);
  262. await waitFor(() => expect(cb).toHaveBeenCalled());
  263. expect(cb.mock.calls[0][0][1].size).toBe(1);
  264. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  265. expect(cb.mock.calls[0][0][2]).toBe(null);
  266. });
  267. it('span.duration (seconds)', async () => {
  268. const tree = makeTree([
  269. makeSpan({start_timestamp: 0, timestamp: 1}),
  270. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  271. ]);
  272. const cb = jest.fn();
  273. search('span.duration:>0.5s', tree, cb);
  274. await waitFor(() => expect(cb).toHaveBeenCalled());
  275. expect(cb.mock.calls[0][0][1].size).toBe(1);
  276. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  277. expect(cb.mock.calls[0][0][2]).toBe(null);
  278. });
  279. it('span.total_time', async () => {
  280. const tree = makeTree([
  281. makeSpan({start_timestamp: 0, timestamp: 1}),
  282. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  283. ]);
  284. const cb = jest.fn();
  285. search('span.total_time:>0.5s', tree, cb);
  286. await waitFor(() => expect(cb).toHaveBeenCalled());
  287. expect(cb.mock.calls[0][0][1].size).toBe(1);
  288. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  289. expect(cb.mock.calls[0][0][2]).toBe(null);
  290. });
  291. it('span.self_time', async () => {
  292. const tree = makeTree([
  293. makeSpan({exclusive_time: 1000}),
  294. makeSpan({exclusive_time: 500}),
  295. ]);
  296. const cb = jest.fn();
  297. search('span.self_time:>0.5s', tree, cb);
  298. await waitFor(() => expect(cb).toHaveBeenCalled());
  299. expect(cb.mock.calls[0][0][1].size).toBe(1);
  300. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  301. expect(cb.mock.calls[0][0][2]).toBe(null);
  302. });
  303. it('span.exclusive_time', async () => {
  304. const tree = makeTree([
  305. makeSpan({exclusive_time: 1000}),
  306. makeSpan({exclusive_time: 500}),
  307. ]);
  308. const cb = jest.fn();
  309. search('span.exclusive_time:>0.5s', tree, cb);
  310. await waitFor(() => expect(cb).toHaveBeenCalled());
  311. expect(cb.mock.calls[0][0][1].size).toBe(1);
  312. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  313. expect(cb.mock.calls[0][0][2]).toBe(null);
  314. });
  315. it('exclusive_time', async () => {
  316. const tree = makeTree([
  317. makeSpan({exclusive_time: 1000}),
  318. makeSpan({exclusive_time: 500}),
  319. ]);
  320. const cb = jest.fn();
  321. search('exclusive_time:>0.5s', tree, cb);
  322. await waitFor(() => expect(cb).toHaveBeenCalled());
  323. expect(cb.mock.calls[0][0][1].size).toBe(1);
  324. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  325. expect(cb.mock.calls[0][0][2]).toBe(null);
  326. });
  327. });
  328. describe('synthetic keys', () => {
  329. describe('has:', () => {
  330. it.each(['error', 'errors'])('%s (transaction)', async key => {
  331. const tree = makeTree([
  332. makeTransaction({
  333. errors: [makeError()],
  334. }),
  335. makeTransaction({errors: []}),
  336. ]);
  337. const cb = jest.fn();
  338. search(`has:${key}`, tree, cb);
  339. await waitFor(() => expect(cb).toHaveBeenCalled());
  340. expect(cb.mock.calls[0][0][1].size).toBe(1);
  341. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  342. expect(cb.mock.calls[0][0][2]).toBe(null);
  343. });
  344. it.each(['issue', 'issues'])('%s (error on transaction)', async key => {
  345. const tree = makeTree([
  346. makeTransaction({
  347. errors: [makeError()],
  348. }),
  349. makeTransaction({errors: []}),
  350. ]);
  351. const cb = jest.fn();
  352. search(`has:${key}`, tree, cb);
  353. await waitFor(() => expect(cb).toHaveBeenCalled());
  354. expect(cb.mock.calls[0][0][1].size).toBe(1);
  355. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  356. expect(cb.mock.calls[0][0][2]).toBe(null);
  357. });
  358. it.each(['issue', 'issues'])('%s (performance issue on transaction)', async key => {
  359. const tree = makeTree([
  360. makeTransaction({
  361. performance_issues: [makePerformanceIssue()],
  362. }),
  363. makeTransaction({errors: []}),
  364. ]);
  365. const cb = jest.fn();
  366. search(`has:${key}`, tree, cb);
  367. await waitFor(() => expect(cb).toHaveBeenCalled());
  368. expect(cb.mock.calls[0][0][1].size).toBe(1);
  369. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  370. expect(cb.mock.calls[0][0][2]).toBe(null);
  371. });
  372. it.each(['profile', 'profiles'])('%s (profile on transaction)', async key => {
  373. const tree = makeTree([
  374. makeTransaction({
  375. profile_id: 'profile',
  376. }),
  377. makeTransaction({errors: []}),
  378. ]);
  379. const cb = jest.fn();
  380. search(`has:${key}`, tree, cb);
  381. await waitFor(() => expect(cb).toHaveBeenCalled());
  382. expect(cb.mock.calls[0][0][1].size).toBe(1);
  383. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  384. expect(cb.mock.calls[0][0][2]).toBe(null);
  385. });
  386. });
  387. });
  388. describe('project aliases', () => {
  389. it('project -> project_slug', async () => {
  390. const tree = makeTree([makeTransaction({project_slug: 'test_project'})]);
  391. const cb = jest.fn();
  392. search('project:test_project', tree, cb);
  393. await waitFor(() => expect(cb).toHaveBeenCalled());
  394. expect(cb.mock.calls[0][0][1].size).toBe(1);
  395. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  396. expect(cb.mock.calls[0][0][2]).toBe(null);
  397. });
  398. it('project.name -> project_slug', async () => {
  399. const tree = makeTree([makeTransaction({project_slug: 'test_project'})]);
  400. const cb = jest.fn();
  401. search('project.name:test_project', tree, cb);
  402. await waitFor(() => expect(cb).toHaveBeenCalled());
  403. expect(cb.mock.calls[0][0][1].size).toBe(1);
  404. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  405. expect(cb.mock.calls[0][0][2]).toBe(null);
  406. });
  407. });
  408. });