traceSearchEvaluator.spec.tsx 14 KB

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