traceSearchEvaluator.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  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]).toBeNull();
  232. });
  233. // For consistency between spans and txns, `transaction.self_time` should be implemented
  234. });
  235. describe('span', () => {
  236. it('text filter', async () => {
  237. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  238. const cb = jest.fn();
  239. search('op:db', tree, cb);
  240. await waitFor(() => expect(cb).toHaveBeenCalled());
  241. expect(cb.mock.calls[0][0][1].size).toBe(1);
  242. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  243. expect(cb.mock.calls[0][0][2]).toBeNull();
  244. });
  245. it('text filter with prefix', async () => {
  246. const tree = makeTree([makeSpan({op: 'db'}), makeSpan({op: 'http'})]);
  247. const cb = jest.fn();
  248. search('span.op:db', tree, cb);
  249. await waitFor(() => expect(cb).toHaveBeenCalled());
  250. expect(cb.mock.calls[0][0][1].size).toBe(1);
  251. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  252. expect(cb.mock.calls[0][0][2]).toBeNull();
  253. });
  254. it('span.duration (milliseconds)', async () => {
  255. const tree = makeTree([
  256. makeSpan({start_timestamp: 0, timestamp: 1}),
  257. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  258. ]);
  259. const cb = jest.fn();
  260. search('span.duration:>500ms', tree, cb);
  261. await waitFor(() => expect(cb).toHaveBeenCalled());
  262. expect(cb.mock.calls[0][0][1].size).toBe(1);
  263. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  264. expect(cb.mock.calls[0][0][2]).toBeNull();
  265. });
  266. it('span.duration (seconds)', async () => {
  267. const tree = makeTree([
  268. makeSpan({start_timestamp: 0, timestamp: 1}),
  269. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  270. ]);
  271. const cb = jest.fn();
  272. search('span.duration:>0.5s', tree, cb);
  273. await waitFor(() => expect(cb).toHaveBeenCalled());
  274. expect(cb.mock.calls[0][0][1].size).toBe(1);
  275. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  276. expect(cb.mock.calls[0][0][2]).toBeNull();
  277. });
  278. it('span.total_time', async () => {
  279. const tree = makeTree([
  280. makeSpan({start_timestamp: 0, timestamp: 1}),
  281. makeSpan({start_timestamp: 0, timestamp: 0.5}),
  282. ]);
  283. const cb = jest.fn();
  284. search('span.total_time:>0.5s', tree, cb);
  285. await waitFor(() => expect(cb).toHaveBeenCalled());
  286. expect(cb.mock.calls[0][0][1].size).toBe(1);
  287. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  288. expect(cb.mock.calls[0][0][2]).toBeNull();
  289. });
  290. it('span.self_time', async () => {
  291. const tree = makeTree([
  292. makeSpan({exclusive_time: 1000}),
  293. makeSpan({exclusive_time: 500}),
  294. ]);
  295. const cb = jest.fn();
  296. search('span.self_time:>0.5s', tree, cb);
  297. await waitFor(() => expect(cb).toHaveBeenCalled());
  298. expect(cb.mock.calls[0][0][1].size).toBe(1);
  299. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  300. expect(cb.mock.calls[0][0][2]).toBeNull();
  301. });
  302. it('span.exclusive_time', async () => {
  303. const tree = makeTree([
  304. makeSpan({exclusive_time: 1000}),
  305. makeSpan({exclusive_time: 500}),
  306. ]);
  307. const cb = jest.fn();
  308. search('span.exclusive_time:>0.5s', tree, cb);
  309. await waitFor(() => expect(cb).toHaveBeenCalled());
  310. expect(cb.mock.calls[0][0][1].size).toBe(1);
  311. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  312. expect(cb.mock.calls[0][0][2]).toBeNull();
  313. });
  314. it('exclusive_time', async () => {
  315. const tree = makeTree([
  316. makeSpan({exclusive_time: 1000}),
  317. makeSpan({exclusive_time: 500}),
  318. ]);
  319. const cb = jest.fn();
  320. search('exclusive_time:>0.5s', tree, cb);
  321. await waitFor(() => expect(cb).toHaveBeenCalled());
  322. expect(cb.mock.calls[0][0][1].size).toBe(1);
  323. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  324. expect(cb.mock.calls[0][0][2]).toBeNull();
  325. });
  326. });
  327. describe('synthetic keys', () => {
  328. describe('has:', () => {
  329. it.each(['error', 'errors'])('%s (transaction)', async key => {
  330. const tree = makeTree([
  331. makeTransaction({
  332. errors: [makeError()],
  333. }),
  334. makeTransaction({errors: []}),
  335. ]);
  336. const cb = jest.fn();
  337. search(`has:${key}`, tree, cb);
  338. await waitFor(() => expect(cb).toHaveBeenCalled());
  339. expect(cb.mock.calls[0][0][1].size).toBe(1);
  340. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  341. expect(cb.mock.calls[0][0][2]).toBeNull();
  342. });
  343. it.each(['issue', 'issues'])('%s (error on transaction)', async key => {
  344. const tree = makeTree([
  345. makeTransaction({
  346. errors: [makeError()],
  347. }),
  348. makeTransaction({errors: []}),
  349. ]);
  350. const cb = jest.fn();
  351. search(`has:${key}`, tree, cb);
  352. await waitFor(() => expect(cb).toHaveBeenCalled());
  353. expect(cb.mock.calls[0][0][1].size).toBe(1);
  354. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  355. expect(cb.mock.calls[0][0][2]).toBeNull();
  356. });
  357. it.each(['issue', 'issues'])('%s (performance issue on transaction)', async key => {
  358. const tree = makeTree([
  359. makeTransaction({
  360. performance_issues: [makePerformanceIssue()],
  361. }),
  362. makeTransaction({errors: []}),
  363. ]);
  364. const cb = jest.fn();
  365. search(`has:${key}`, tree, cb);
  366. await waitFor(() => expect(cb).toHaveBeenCalled());
  367. expect(cb.mock.calls[0][0][1].size).toBe(1);
  368. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  369. expect(cb.mock.calls[0][0][2]).toBeNull();
  370. });
  371. it.each(['profile', 'profiles'])('%s (profile on transaction)', async key => {
  372. const tree = makeTree([
  373. makeTransaction({
  374. profile_id: 'profile',
  375. }),
  376. makeTransaction({errors: []}),
  377. ]);
  378. const cb = jest.fn();
  379. search(`has:${key}`, tree, cb);
  380. await waitFor(() => expect(cb).toHaveBeenCalled());
  381. expect(cb.mock.calls[0][0][1].size).toBe(1);
  382. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  383. expect(cb.mock.calls[0][0][2]).toBeNull();
  384. });
  385. });
  386. });
  387. describe('project aliases', () => {
  388. it('project -> project_slug', async () => {
  389. const tree = makeTree([makeTransaction({project_slug: 'test_project'})]);
  390. const cb = jest.fn();
  391. search('project:test_project', tree, cb);
  392. await waitFor(() => expect(cb).toHaveBeenCalled());
  393. expect(cb.mock.calls[0][0][1].size).toBe(1);
  394. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  395. expect(cb.mock.calls[0][0][2]).toBeNull();
  396. });
  397. it('project.name -> project_slug', async () => {
  398. const tree = makeTree([makeTransaction({project_slug: 'test_project'})]);
  399. const cb = jest.fn();
  400. search('project.name:test_project', tree, cb);
  401. await waitFor(() => expect(cb).toHaveBeenCalled());
  402. expect(cb.mock.calls[0][0][1].size).toBe(1);
  403. expect(cb.mock.calls[0][0][0]).toEqual([{index: 0, value: tree.list[0]}]);
  404. expect(cb.mock.calls[0][0][2]).toBeNull();
  405. });
  406. });
  407. });