tokenizeSearch.spec.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import {MutableSearch, TokenType} from 'sentry/utils/tokenizeSearch';
  2. describe('utils/tokenizeSearch', function () {
  3. describe('MutableSearch.fromQueryObject', function () {
  4. it.each([
  5. [{transaction: '/index'}, 'transaction:/index'],
  6. [{transaction: '/index', has: 'span.domain'}, 'transaction:/index has:span.domain'],
  7. [{transaction: '/index', 'span.domain': undefined}, 'transaction:/index'],
  8. [{'span.domain': '*hello*'}, 'span.domain:*hello*'],
  9. [{'span.description': '*hello*'}, 'span.description:*hello*'],
  10. [{'span.duration': ['>0', '<100']}, 'span.duration:>0 span.duration:<100'],
  11. [{transaction: '(empty)'}, '!has:transaction'],
  12. ])('converts %s to search string', (query, result) => {
  13. expect(MutableSearch.fromQueryObject(query).formatString()).toEqual(result);
  14. });
  15. });
  16. describe('new MutableSearch()', function () {
  17. const cases = [
  18. {
  19. name: 'should convert a basic query string to a query object',
  20. string: 'is:unresolved',
  21. object: {
  22. tokens: [{type: TokenType.FILTER, key: 'is', value: 'unresolved'}],
  23. },
  24. },
  25. {
  26. name: 'should convert quoted strings',
  27. string: 'is:unresolved browser:"Chrome 36"',
  28. object: {
  29. tokens: [
  30. {type: TokenType.FILTER, key: 'is', value: 'unresolved'},
  31. {type: TokenType.FILTER, key: 'browser', value: 'Chrome 36'},
  32. ],
  33. },
  34. },
  35. {
  36. name: 'should populate the text query',
  37. string: 'python is:unresolved browser:"Chrome 36"',
  38. object: {
  39. tokens: [
  40. {type: TokenType.FREE_TEXT, value: 'python'},
  41. {type: TokenType.FILTER, key: 'is', value: 'unresolved'},
  42. {type: TokenType.FILTER, key: 'browser', value: 'Chrome 36'},
  43. ],
  44. },
  45. },
  46. {
  47. name: 'should tokenize the text query',
  48. string: 'python exception',
  49. object: {
  50. tokens: [
  51. {type: TokenType.FREE_TEXT, value: 'python'},
  52. {type: TokenType.FREE_TEXT, value: 'exception'},
  53. ],
  54. },
  55. },
  56. {
  57. name: 'should tokenize has condition',
  58. string: 'has:user has:browser',
  59. object: {
  60. tokens: [
  61. {type: TokenType.FILTER, key: 'has', value: 'user'},
  62. {type: TokenType.FILTER, key: 'has', value: 'browser'},
  63. ],
  64. },
  65. },
  66. {
  67. name: 'should tokenize !has condition',
  68. string: '!has:user has:browser',
  69. object: {
  70. tokens: [
  71. {type: TokenType.FILTER, key: '!has', value: 'user'},
  72. {type: TokenType.FILTER, key: 'has', value: 'browser'},
  73. ],
  74. },
  75. },
  76. {
  77. name: 'should remove spaces in the query',
  78. string: 'python is:unresolved exception',
  79. object: {
  80. tokens: [
  81. {type: TokenType.FREE_TEXT, value: 'python'},
  82. {type: TokenType.FILTER, key: 'is', value: 'unresolved'},
  83. {type: TokenType.FREE_TEXT, value: 'exception'},
  84. ],
  85. },
  86. },
  87. {
  88. name: 'should tokenize the quoted tags',
  89. string: 'event.type:error title:"QueryExecutionError: Code: 141."',
  90. object: {
  91. tokens: [
  92. {type: TokenType.FILTER, key: 'event.type', value: 'error'},
  93. {
  94. type: TokenType.FILTER,
  95. key: 'title',
  96. value: 'QueryExecutionError: Code: 141.',
  97. },
  98. ],
  99. },
  100. },
  101. {
  102. name: 'should tokenize words with :: in them',
  103. string: 'key:Resque::DirtyExit',
  104. object: {
  105. tokens: [{type: TokenType.FILTER, key: 'key', value: 'Resque::DirtyExit'}],
  106. },
  107. },
  108. {
  109. name: 'tokens that begin with a colon are still queries',
  110. string: 'country:canada :unresolved',
  111. object: {
  112. tokens: [
  113. {type: TokenType.FILTER, key: 'country', value: 'canada'},
  114. {type: TokenType.FREE_TEXT, value: ':unresolved'},
  115. ],
  116. },
  117. },
  118. {
  119. name: 'correctly preserve boolean operators',
  120. string: 'country:canada Or country:newzealand',
  121. object: {
  122. tokens: [
  123. {type: TokenType.FILTER, key: 'country', value: 'canada'},
  124. {type: TokenType.OPERATOR, value: 'OR'},
  125. {type: TokenType.FILTER, key: 'country', value: 'newzealand'},
  126. ],
  127. },
  128. },
  129. {
  130. name: 'correctly preserve parens',
  131. string: '(country:canada Or country:newzealand) AnD province:pei',
  132. object: {
  133. tokens: [
  134. {type: TokenType.OPERATOR, value: '('},
  135. {type: TokenType.FILTER, key: 'country', value: 'canada'},
  136. {type: TokenType.OPERATOR, value: 'OR'},
  137. {type: TokenType.FILTER, key: 'country', value: 'newzealand'},
  138. {type: TokenType.OPERATOR, value: ')'},
  139. {type: TokenType.OPERATOR, value: 'AND'},
  140. {type: TokenType.FILTER, key: 'province', value: 'pei'},
  141. ],
  142. },
  143. },
  144. {
  145. name: 'query tags boolean and parens are all stitched back together correctly',
  146. string: '(a:a OR (b:b AND c d e)) OR f g:g',
  147. object: {
  148. tokens: [
  149. {type: TokenType.OPERATOR, value: '('},
  150. {type: TokenType.FILTER, key: 'a', value: 'a'},
  151. {type: TokenType.OPERATOR, value: 'OR'},
  152. {type: TokenType.OPERATOR, value: '('},
  153. {type: TokenType.FILTER, key: 'b', value: 'b'},
  154. {type: TokenType.OPERATOR, value: 'AND'},
  155. {type: TokenType.FREE_TEXT, value: 'c'},
  156. {type: TokenType.FREE_TEXT, value: 'd'},
  157. {type: TokenType.FREE_TEXT, value: 'e'},
  158. {type: TokenType.OPERATOR, value: ')'},
  159. {type: TokenType.OPERATOR, value: ')'},
  160. {type: TokenType.OPERATOR, value: 'OR'},
  161. {type: TokenType.FREE_TEXT, value: 'f'},
  162. {type: TokenType.FILTER, key: 'g', value: 'g'},
  163. ],
  164. },
  165. },
  166. {
  167. name: 'correctly preserve filters with functions',
  168. string: 'country:>canada OR coronaFree():<newzealand',
  169. object: {
  170. tokens: [
  171. {type: TokenType.FILTER, key: 'country', value: '>canada'},
  172. {type: TokenType.OPERATOR, value: 'OR'},
  173. {type: TokenType.FILTER, key: 'coronaFree()', value: '<newzealand'},
  174. ],
  175. },
  176. },
  177. {
  178. name: 'correctly preserves leading/trailing escaped quotes',
  179. string: 'a:"\\"a\\""',
  180. object: {
  181. tokens: [{type: TokenType.FILTER, key: 'a', value: '\\"a\\"'}],
  182. },
  183. },
  184. {
  185. name: 'correctly tokenizes escaped quotes',
  186. string: 'a:"i \\" quote" b:"b\\"bb" c:"cc"',
  187. object: {
  188. tokens: [
  189. {type: TokenType.FILTER, key: 'a', value: 'i \\" quote'},
  190. {type: TokenType.FILTER, key: 'b', value: 'b\\"bb'},
  191. {type: TokenType.FILTER, key: 'c', value: 'cc'},
  192. ],
  193. },
  194. },
  195. ];
  196. for (const {name, string, object} of cases) {
  197. // eslint-disable-next-line jest/valid-title
  198. it(name, () => expect(new MutableSearch(string)).toEqual(object));
  199. }
  200. });
  201. describe('QueryResults operations', function () {
  202. it('add tokens to query object', function () {
  203. const results = new MutableSearch([]);
  204. results.addStringFilter('a:a');
  205. expect(results.formatString()).toEqual('a:a');
  206. results.addFilterValues('b', ['b']);
  207. expect(results.formatString()).toEqual('a:a b:b');
  208. results.addFilterValues('c', ['c1', 'c2']);
  209. expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2');
  210. results.addFilterValues('d', ['d']);
  211. expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2 d:d');
  212. results.addFilterValues('e', ['e1*e2\\e3']);
  213. expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2 d:d e:"e1\\*e2\\e3"');
  214. results.addStringFilter('d:d2');
  215. expect(results.formatString()).toEqual(
  216. 'a:a b:b c:c1 c:c2 d:d e:"e1\\*e2\\e3" d:d2'
  217. );
  218. });
  219. it('adds individual values to query object', function () {
  220. const results = new MutableSearch([]);
  221. results.addFilterValue('e', 'e1*e2\\e3');
  222. expect(results.formatString()).toEqual('e:"e1\\*e2\\e3"');
  223. });
  224. it('add text searches to query object', function () {
  225. const results = new MutableSearch(['a:a']);
  226. results.addFreeText('b');
  227. expect(results.formatString()).toEqual('a:a b');
  228. expect(results.freeText).toEqual(['b']);
  229. results.addFreeText('c');
  230. expect(results.formatString()).toEqual('a:a b c');
  231. expect(results.freeText).toEqual(['b', 'c']);
  232. results.addStringFilter('d:d').addFreeText('e');
  233. expect(results.formatString()).toEqual('a:a b c d:d e');
  234. expect(results.freeText).toEqual(['b', 'c', 'e']);
  235. results.freeText = ['x', 'y'];
  236. expect(results.formatString()).toEqual('a:a d:d x y');
  237. expect(results.freeText).toEqual(['x', 'y']);
  238. results.freeText = ['a b c'];
  239. expect(results.formatString()).toEqual('a:a d:d "a b c"');
  240. expect(results.freeText).toEqual(['a b c']);
  241. results.freeText = ['invalid literal for int() with base'];
  242. expect(results.formatString()).toEqual(
  243. 'a:a d:d "invalid literal for int() with base"'
  244. );
  245. expect(results.freeText).toEqual(['invalid literal for int() with base']);
  246. });
  247. it('add ops to query object', function () {
  248. const results = new MutableSearch(['x', 'a:a', 'y']);
  249. results.addOp('OR');
  250. expect(results.formatString()).toEqual('x a:a y OR');
  251. results.addFreeText('z');
  252. expect(results.formatString()).toEqual('x a:a y OR z');
  253. results
  254. .addOp('(')
  255. .addStringFilter('b:b')
  256. .addOp('AND')
  257. .addStringFilter('c:c')
  258. .addOp(')');
  259. expect(results.formatString()).toEqual('x a:a y OR z ( b:b AND c:c )');
  260. });
  261. it('adds tags to query', function () {
  262. const results = new MutableSearch(['tag:value']);
  263. results.addStringFilter('new:too');
  264. expect(results.formatString()).toEqual('tag:value new:too');
  265. });
  266. it('setTag() replaces tags', function () {
  267. const results = new MutableSearch(['tag:value']);
  268. results.setFilterValues('tag', ['too']);
  269. expect(results.formatString()).toEqual('tag:too');
  270. });
  271. it('setTag() replaces tags in OR', function () {
  272. let results = new MutableSearch([
  273. '(',
  274. 'transaction:xyz',
  275. 'OR',
  276. 'transaction:abc',
  277. ')',
  278. ]);
  279. results.setFilterValues('transaction', ['def']);
  280. expect(results.formatString()).toEqual('transaction:def');
  281. results = new MutableSearch(['(transaction:xyz', 'OR', 'transaction:abc)']);
  282. results.setFilterValues('transaction', ['def']);
  283. expect(results.formatString()).toEqual('transaction:def');
  284. });
  285. it('does not remove boolean operators after setting tag values', function () {
  286. const results = new MutableSearch([
  287. '(',
  288. 'start:xyz',
  289. 'AND',
  290. 'end:abc',
  291. ')',
  292. 'OR',
  293. '(',
  294. 'start:abc',
  295. 'AND',
  296. 'end:xyz',
  297. ')',
  298. ]);
  299. results.setFilterValues('transaction', ['def']);
  300. expect(results.formatString()).toEqual(
  301. '( start:xyz AND end:abc ) OR ( start:abc AND end:xyz ) transaction:def'
  302. );
  303. });
  304. it('removes tags from query object', function () {
  305. let results = new MutableSearch(['x', 'a:a', 'b:b']);
  306. results.removeFilter('a');
  307. expect(results.formatString()).toEqual('x b:b');
  308. results = new MutableSearch(['a:a']);
  309. results.removeFilter('a');
  310. expect(results.formatString()).toEqual('');
  311. results = new MutableSearch(['x', 'a:a', 'a:a2']);
  312. results.removeFilter('a');
  313. expect(results.formatString()).toEqual('x');
  314. results = new MutableSearch(['a:a', 'OR', 'b:b']);
  315. results.removeFilter('a');
  316. expect(results.formatString()).toEqual('b:b');
  317. results = new MutableSearch(['a:a', 'OR', 'a:a1', 'AND', 'b:b']);
  318. results.removeFilter('a');
  319. expect(results.formatString()).toEqual('b:b');
  320. results = new MutableSearch(['(a:a', 'OR', 'b:b)']);
  321. results.removeFilter('a');
  322. expect(results.formatString()).toEqual('b:b');
  323. results = new MutableSearch(['(a:a', 'OR', 'b:b', 'OR', 'y)']);
  324. results.removeFilter('a');
  325. expect(results.formatString()).toEqual('( b:b OR y )');
  326. results = new MutableSearch(['(a:a', 'OR', '(b:b1', 'OR', '(c:c', 'OR', 'b:b2)))']);
  327. results.removeFilter('b');
  328. expect(results.formatString()).toEqual('( a:a OR c:c )');
  329. results = new MutableSearch(['(((a:a', 'OR', 'b:b1)', 'OR', 'c:c)', 'OR', 'b:b2)']);
  330. results.removeFilter('b');
  331. expect(results.formatString()).toEqual('( ( a:a OR c:c ) )');
  332. results = new MutableSearch(['a:a', '(b:b1', 'OR', 'b:b2', 'OR', 'b:b3)', 'c:c']);
  333. results.removeFilter('b');
  334. expect(results.formatString()).toEqual('a:a c:c');
  335. });
  336. it('can return the tag keys', function () {
  337. const results = new MutableSearch(['tag:value', 'other:value', 'additional text']);
  338. expect(results.getFilterKeys()).toEqual(['tag', 'other']);
  339. });
  340. it('getTagValues', () => {
  341. const results = new MutableSearch([
  342. 'tag:value',
  343. 'other:value',
  344. 'tag:value2',
  345. 'additional text',
  346. ]);
  347. expect(results.getFilterValues('tag')).toEqual(['value', 'value2']);
  348. expect(results.getFilterValues('nonexistent')).toEqual([]);
  349. });
  350. });
  351. describe('QueryResults.formatString', function () {
  352. const cases = [
  353. {
  354. name: 'should convert a basic object to a query string',
  355. object: new MutableSearch(['is:unresolved']),
  356. string: 'is:unresolved',
  357. },
  358. {
  359. name: 'should quote tags with spaces',
  360. object: new MutableSearch(['is:unresolved', 'browser:"Chrome 36"']),
  361. string: 'is:unresolved browser:"Chrome 36"',
  362. },
  363. {
  364. name: 'should stringify the query',
  365. object: new MutableSearch(['python', 'is:unresolved', 'browser:"Chrome 36"']),
  366. string: 'python is:unresolved browser:"Chrome 36"',
  367. },
  368. {
  369. name: 'should join tokenized queries',
  370. object: new MutableSearch(['python', 'exception']),
  371. string: 'python exception',
  372. },
  373. {
  374. name: 'should quote tags with spaces',
  375. object: new MutableSearch([
  376. 'oh',
  377. 'me',
  378. 'oh',
  379. 'my',
  380. 'browser:"Chrome 36"',
  381. 'browser:"Firefox 60"',
  382. ]),
  383. string: 'oh me oh my browser:"Chrome 36" browser:"Firefox 60"',
  384. },
  385. {
  386. name: 'should quote tags with parens',
  387. object: new MutableSearch([
  388. 'bad',
  389. 'things',
  390. 'repository_id:"UUID(\'long-value\')"',
  391. ]),
  392. string: 'bad things repository_id:"UUID(\'long-value\')"',
  393. },
  394. {
  395. // values with quotes do not need to be quoted
  396. // furthermore, timestamps contain colons
  397. // but the backend currently does not support quoted date formats
  398. name: 'should not quote tags with colon',
  399. object: new MutableSearch(['bad', 'things', 'user:"id:123"']),
  400. string: 'bad things user:id:123',
  401. },
  402. {
  403. name: 'should escape quote tags with double quotes',
  404. object: new MutableSearch([
  405. 'bad',
  406. 'things',
  407. 'name:"Ernest \\"Papa\\" Hemingway"',
  408. ]),
  409. string: 'bad things name:"Ernest \\"Papa\\" Hemingway"',
  410. },
  411. {
  412. name: 'should include blank strings',
  413. object: new MutableSearch(['bad', 'things', 'name:""']),
  414. string: 'bad things name:""',
  415. },
  416. {
  417. name: 'correctly preserve boolean operators',
  418. object: new MutableSearch(['country:canada', 'OR', 'country:newzealand']),
  419. string: 'country:canada OR country:newzealand',
  420. },
  421. {
  422. name: 'correctly preserve parens',
  423. object: new MutableSearch([
  424. '(country:canada',
  425. 'OR',
  426. 'country:newzealand)',
  427. 'AND',
  428. 'province:pei',
  429. ]),
  430. string: '( country:canada OR country:newzealand ) AND province:pei',
  431. },
  432. {
  433. name: 'query tags boolean and parens are all stitched back together correctly',
  434. object: new MutableSearch([
  435. '(a:a',
  436. 'OR',
  437. '(b:b',
  438. 'AND',
  439. 'c',
  440. 'd',
  441. 'e))',
  442. 'OR',
  443. 'f',
  444. 'g:g',
  445. ]),
  446. string: '( a:a OR ( b:b AND c d e ) ) OR f g:g',
  447. },
  448. {
  449. name: 'correctly preserve filters with functions',
  450. object: new MutableSearch(['country:>canada', 'OR', 'coronaFree():<newzealand']),
  451. string: 'country:>canada OR coronaFree():<newzealand',
  452. },
  453. {
  454. name: 'should quote tags with parens and spaces',
  455. object: new MutableSearch(['release:4.9.0 build (0.0.01)', 'error.handled:0']),
  456. string: 'release:"4.9.0 build (0.0.01)" error.handled:0',
  457. },
  458. ];
  459. for (const {name, string, object} of cases) {
  460. // eslint-disable-next-line jest/valid-title
  461. it(name, () => expect(object.formatString()).toEqual(string));
  462. }
  463. });
  464. });