parser.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  1. import moment from 'moment';
  2. import {LocationRange} from 'pegjs';
  3. import {t} from 'sentry/locale';
  4. import {TagCollection} from 'sentry/types';
  5. import {
  6. isMeasurement,
  7. isSpanOperationBreakdownField,
  8. measurementType,
  9. } from 'sentry/utils/discover/fields';
  10. import grammar from './grammar.pegjs';
  11. import {getKeyName} from './utils';
  12. type TextFn = () => string;
  13. type LocationFn = () => LocationRange;
  14. type ListItem<V> = [
  15. space: ReturnType<TokenConverter['tokenSpaces']>,
  16. comma: string,
  17. space: ReturnType<TokenConverter['tokenSpaces']>,
  18. notComma: undefined,
  19. value: V | null
  20. ];
  21. const listJoiner = <K,>([s1, comma, s2, _, value]: ListItem<K>) => ({
  22. separator: [s1.value, comma, s2.value].join(''),
  23. value,
  24. });
  25. /**
  26. * A token represents a node in the syntax tree. These are all extrapolated
  27. * from the grammar and may not be named exactly the same.
  28. */
  29. export enum Token {
  30. Spaces = 'spaces',
  31. Filter = 'filter',
  32. FreeText = 'freeText',
  33. LogicGroup = 'logicGroup',
  34. LogicBoolean = 'logicBoolean',
  35. KeySimple = 'keySimple',
  36. KeyExplicitTag = 'keyExplicitTag',
  37. KeyAggregate = 'keyAggregate',
  38. KeyAggregateArgs = 'keyAggregateArgs',
  39. KeyAggregateParam = 'keyAggregateParam',
  40. ValueIso8601Date = 'valueIso8601Date',
  41. ValueRelativeDate = 'valueRelativeDate',
  42. ValueDuration = 'valueDuration',
  43. ValueSize = 'valueSize',
  44. ValuePercentage = 'valuePercentage',
  45. ValueBoolean = 'valueBoolean',
  46. ValueNumber = 'valueNumber',
  47. ValueText = 'valueText',
  48. ValueNumberList = 'valueNumberList',
  49. ValueTextList = 'valueTextList',
  50. }
  51. /**
  52. * An operator in a key value term
  53. */
  54. export enum TermOperator {
  55. Default = '',
  56. GreaterThanEqual = '>=',
  57. LessThanEqual = '<=',
  58. GreaterThan = '>',
  59. LessThan = '<',
  60. Equal = '=',
  61. NotEqual = '!=',
  62. }
  63. /**
  64. * Logic operators
  65. */
  66. export enum BooleanOperator {
  67. And = 'AND',
  68. Or = 'OR',
  69. }
  70. /**
  71. * The Token.Filter may be one of many types of filters. This enum declares the
  72. * each variant filter type.
  73. */
  74. export enum FilterType {
  75. Text = 'text',
  76. TextIn = 'textIn',
  77. Date = 'date',
  78. SpecificDate = 'specificDate',
  79. RelativeDate = 'relativeDate',
  80. Duration = 'duration',
  81. Size = 'size',
  82. Numeric = 'numeric',
  83. NumericIn = 'numericIn',
  84. Boolean = 'boolean',
  85. AggregateDuration = 'aggregateDuration',
  86. AggregateSize = 'aggregateSize',
  87. AggregatePercentage = 'aggregatePercentage',
  88. AggregateNumeric = 'aggregateNumeric',
  89. AggregateDate = 'aggregateDate',
  90. AggregateRelativeDate = 'aggregateRelativeDate',
  91. Has = 'has',
  92. Is = 'is',
  93. }
  94. export const allOperators = [
  95. TermOperator.Default,
  96. TermOperator.GreaterThanEqual,
  97. TermOperator.LessThanEqual,
  98. TermOperator.GreaterThan,
  99. TermOperator.LessThan,
  100. TermOperator.Equal,
  101. TermOperator.NotEqual,
  102. ] as const;
  103. const basicOperators = [TermOperator.Default, TermOperator.NotEqual] as const;
  104. /**
  105. * Map of certain filter types to other filter types with applicable operators
  106. * e.g. SpecificDate can use the operators from Date to become a Date filter.
  107. */
  108. export const interchangeableFilterOperators = {
  109. [FilterType.SpecificDate]: [FilterType.Date],
  110. [FilterType.Date]: [FilterType.SpecificDate],
  111. };
  112. const textKeys = [Token.KeySimple, Token.KeyExplicitTag] as const;
  113. const numberUnits = {
  114. b: 1_000_000_000,
  115. m: 1_000_000,
  116. k: 1_000,
  117. };
  118. /**
  119. * This constant-type configuration object declares how each filter type
  120. * operates. Including what types of keys, operators, and values it may
  121. * receive.
  122. *
  123. * This configuration is used to generate the discriminate Filter type that is
  124. * returned from the tokenFilter converter.
  125. */
  126. export const filterTypeConfig = {
  127. [FilterType.Text]: {
  128. validKeys: textKeys,
  129. validOps: basicOperators,
  130. validValues: [Token.ValueText],
  131. canNegate: true,
  132. },
  133. [FilterType.TextIn]: {
  134. validKeys: textKeys,
  135. validOps: [],
  136. validValues: [Token.ValueTextList],
  137. canNegate: true,
  138. },
  139. [FilterType.Date]: {
  140. validKeys: [Token.KeySimple],
  141. validOps: allOperators,
  142. validValues: [Token.ValueIso8601Date],
  143. canNegate: false,
  144. },
  145. [FilterType.SpecificDate]: {
  146. validKeys: [Token.KeySimple],
  147. validOps: [],
  148. validValues: [Token.ValueIso8601Date],
  149. canNegate: false,
  150. },
  151. [FilterType.RelativeDate]: {
  152. validKeys: [Token.KeySimple],
  153. validOps: [],
  154. validValues: [Token.ValueRelativeDate],
  155. canNegate: false,
  156. },
  157. [FilterType.Duration]: {
  158. validKeys: [Token.KeySimple],
  159. validOps: allOperators,
  160. validValues: [Token.ValueDuration],
  161. canNegate: true,
  162. },
  163. [FilterType.Size]: {
  164. validKeys: [Token.KeySimple],
  165. validOps: allOperators,
  166. validValues: [Token.ValueSize],
  167. canNegate: true,
  168. },
  169. [FilterType.Numeric]: {
  170. validKeys: [Token.KeySimple],
  171. validOps: allOperators,
  172. validValues: [Token.ValueNumber],
  173. canNegate: true,
  174. },
  175. [FilterType.NumericIn]: {
  176. validKeys: [Token.KeySimple],
  177. validOps: [],
  178. validValues: [Token.ValueNumberList],
  179. canNegate: true,
  180. },
  181. [FilterType.Boolean]: {
  182. validKeys: [Token.KeySimple],
  183. validOps: basicOperators,
  184. validValues: [Token.ValueBoolean],
  185. canNegate: true,
  186. },
  187. [FilterType.AggregateDuration]: {
  188. validKeys: [Token.KeyAggregate],
  189. validOps: allOperators,
  190. validValues: [Token.ValueDuration],
  191. canNegate: true,
  192. },
  193. [FilterType.AggregateSize]: {
  194. validKeys: [Token.KeyAggregate],
  195. validOps: allOperators,
  196. validValues: [Token.ValueSize],
  197. canNegate: true,
  198. },
  199. [FilterType.AggregateNumeric]: {
  200. validKeys: [Token.KeyAggregate],
  201. validOps: allOperators,
  202. validValues: [Token.ValueNumber],
  203. canNegate: true,
  204. },
  205. [FilterType.AggregatePercentage]: {
  206. validKeys: [Token.KeyAggregate],
  207. validOps: allOperators,
  208. validValues: [Token.ValuePercentage],
  209. canNegate: true,
  210. },
  211. [FilterType.AggregateDate]: {
  212. validKeys: [Token.KeyAggregate],
  213. validOps: allOperators,
  214. validValues: [Token.ValueIso8601Date],
  215. canNegate: true,
  216. },
  217. [FilterType.AggregateRelativeDate]: {
  218. validKeys: [Token.KeyAggregate],
  219. validOps: allOperators,
  220. validValues: [Token.ValueRelativeDate],
  221. canNegate: true,
  222. },
  223. [FilterType.Has]: {
  224. validKeys: [Token.KeySimple],
  225. validOps: basicOperators,
  226. validValues: [],
  227. canNegate: true,
  228. },
  229. [FilterType.Is]: {
  230. validKeys: [Token.KeySimple],
  231. validOps: basicOperators,
  232. validValues: [Token.ValueText],
  233. canNegate: true,
  234. },
  235. } as const;
  236. type FilterTypeConfig = typeof filterTypeConfig;
  237. /**
  238. * Object representing an invalid filter state
  239. */
  240. type InvalidFilter = {
  241. /**
  242. * The message indicating why the filter is invalid
  243. */
  244. reason: string;
  245. /**
  246. * In the case where a filter is invalid, we may be expecting a different
  247. * type for this filter based on the key. This can be useful to hint to the
  248. * user what values they should be providing.
  249. *
  250. * This may be multiple filter types.
  251. */
  252. expectedType?: FilterType[];
  253. };
  254. type FilterMap = {
  255. [F in keyof FilterTypeConfig]: {
  256. /**
  257. * The filter type being represented
  258. */
  259. filter: F;
  260. /**
  261. * When a filter is marked as 'invalid' a reason is given. If the filter is
  262. * not invalid this will always be null
  263. */
  264. invalid: InvalidFilter | null;
  265. /**
  266. * The key of the filter
  267. */
  268. key: KVConverter<FilterTypeConfig[F]['validKeys'][number]>;
  269. /**
  270. * Indicates if the filter has been negated
  271. */
  272. negated: FilterTypeConfig[F]['canNegate'] extends true ? boolean : false;
  273. /**
  274. * The operator applied to the filter
  275. */
  276. operator: FilterTypeConfig[F]['validOps'][number];
  277. type: Token.Filter;
  278. /**
  279. * The value of the filter
  280. */
  281. value: KVConverter<FilterTypeConfig[F]['validValues'][number]>;
  282. };
  283. };
  284. type TextFilter = FilterMap[FilterType.Text];
  285. type InFilter = FilterMap[FilterType.TextIn] | FilterMap[FilterType.NumericIn];
  286. /**
  287. * The Filter type discriminates on the FilterType enum using the `filter` key.
  288. *
  289. * When receiving this type you may narrow it to a specific filter by checking
  290. * this field. This will give you proper types on what the key, value, and
  291. * operator results are.
  292. */
  293. type FilterResult = FilterMap[FilterType];
  294. type TokenConverterOpts = {
  295. config: SearchConfig;
  296. location: LocationFn;
  297. text: TextFn;
  298. };
  299. /**
  300. * Used to construct token results via the token grammar
  301. */
  302. export class TokenConverter {
  303. text: TextFn;
  304. location: LocationFn;
  305. config: SearchConfig;
  306. constructor({text, location, config}: TokenConverterOpts) {
  307. this.text = text;
  308. this.location = location;
  309. this.config = config;
  310. }
  311. /**
  312. * Validates various types of keys
  313. */
  314. keyValidation = {
  315. isNumeric: (key: string) =>
  316. this.config.numericKeys.has(key) ||
  317. isMeasurement(key) ||
  318. isSpanOperationBreakdownField(key),
  319. isBoolean: (key: string) => this.config.booleanKeys.has(key),
  320. isPercentage: (key: string) => this.config.percentageKeys.has(key),
  321. isDate: (key: string) => this.config.dateKeys.has(key),
  322. isDuration: (key: string) =>
  323. this.config.durationKeys.has(key) ||
  324. isSpanOperationBreakdownField(key) ||
  325. measurementType(key) === 'duration',
  326. isSize: (key: string) => this.config.sizeKeys.has(key),
  327. };
  328. /**
  329. * Creates shared `text` and `location` keys.
  330. */
  331. get defaultTokenFields() {
  332. return {
  333. text: this.text(),
  334. location: this.location(),
  335. };
  336. }
  337. tokenSpaces = (value: string) => ({
  338. ...this.defaultTokenFields,
  339. type: Token.Spaces as const,
  340. value,
  341. });
  342. tokenFilter = <T extends FilterType>(
  343. filter: T,
  344. key: FilterMap[T]['key'],
  345. value: FilterMap[T]['value'],
  346. operator: FilterMap[T]['operator'] | undefined,
  347. negated: FilterMap[T]['negated']
  348. ) => {
  349. const filterToken = {
  350. type: Token.Filter as const,
  351. filter,
  352. key,
  353. value,
  354. negated,
  355. operator: operator ?? TermOperator.Default,
  356. invalid: this.checkInvalidFilter(filter, key, value),
  357. } as FilterResult;
  358. return {
  359. ...this.defaultTokenFields,
  360. ...filterToken,
  361. };
  362. };
  363. tokenFreeText = (value: string, quoted: boolean) => ({
  364. ...this.defaultTokenFields,
  365. type: Token.FreeText as const,
  366. value,
  367. quoted,
  368. });
  369. tokenLogicGroup = (
  370. inner: Array<
  371. | ReturnType<TokenConverter['tokenLogicBoolean']>
  372. | ReturnType<TokenConverter['tokenFilter']>
  373. | ReturnType<TokenConverter['tokenFreeText']>
  374. >
  375. ) => ({
  376. ...this.defaultTokenFields,
  377. type: Token.LogicGroup as const,
  378. inner,
  379. });
  380. tokenLogicBoolean = (bool: BooleanOperator) => ({
  381. ...this.defaultTokenFields,
  382. type: Token.LogicBoolean as const,
  383. value: bool,
  384. });
  385. tokenKeySimple = (value: string, quoted: boolean) => ({
  386. ...this.defaultTokenFields,
  387. type: Token.KeySimple as const,
  388. value,
  389. quoted,
  390. });
  391. tokenKeyExplicitTag = (
  392. prefix: string,
  393. key: ReturnType<TokenConverter['tokenKeySimple']>
  394. ) => ({
  395. ...this.defaultTokenFields,
  396. type: Token.KeyExplicitTag as const,
  397. prefix,
  398. key,
  399. });
  400. tokenKeyAggregateParam = (value: string, quoted: boolean) => ({
  401. ...this.defaultTokenFields,
  402. type: Token.KeyAggregateParam as const,
  403. value,
  404. quoted,
  405. });
  406. tokenKeyAggregate = (
  407. name: ReturnType<TokenConverter['tokenKeySimple']>,
  408. args: ReturnType<TokenConverter['tokenKeyAggregateArgs']> | null,
  409. argsSpaceBefore: ReturnType<TokenConverter['tokenSpaces']>,
  410. argsSpaceAfter: ReturnType<TokenConverter['tokenSpaces']>
  411. ) => ({
  412. ...this.defaultTokenFields,
  413. type: Token.KeyAggregate as const,
  414. name,
  415. args,
  416. argsSpaceBefore,
  417. argsSpaceAfter,
  418. });
  419. tokenKeyAggregateArgs = (
  420. arg1: ReturnType<TokenConverter['tokenKeyAggregateParam']>,
  421. args: ListItem<ReturnType<TokenConverter['tokenKeyAggregateParam']>>[]
  422. ) => ({
  423. ...this.defaultTokenFields,
  424. type: Token.KeyAggregateArgs as const,
  425. args: [{separator: '', value: arg1}, ...args.map(listJoiner)],
  426. });
  427. tokenValueIso8601Date = (value: string) => ({
  428. ...this.defaultTokenFields,
  429. type: Token.ValueIso8601Date as const,
  430. value: moment(value),
  431. });
  432. tokenValueRelativeDate = (
  433. value: string,
  434. sign: '-' | '+',
  435. unit: 'w' | 'd' | 'h' | 'm'
  436. ) => ({
  437. ...this.defaultTokenFields,
  438. type: Token.ValueRelativeDate as const,
  439. value: Number(value),
  440. sign,
  441. unit,
  442. });
  443. tokenValueDuration = (
  444. value: string,
  445. unit: 'ms' | 's' | 'min' | 'm' | 'hr' | 'h' | 'day' | 'd' | 'wk' | 'w'
  446. ) => ({
  447. ...this.defaultTokenFields,
  448. type: Token.ValueDuration as const,
  449. value: Number(value),
  450. unit,
  451. });
  452. tokenValueSize = (
  453. value: string,
  454. unit:
  455. | 'bit'
  456. | 'nb'
  457. | 'bytes'
  458. | 'kb'
  459. | 'mb'
  460. | 'gb'
  461. | 'tb'
  462. | 'pb'
  463. | 'eb'
  464. | 'zb'
  465. | 'yb'
  466. | 'kib'
  467. | 'mib'
  468. | 'gib'
  469. | 'tib'
  470. | 'pib'
  471. | 'eib'
  472. | 'zib'
  473. | 'yib'
  474. ) => ({
  475. ...this.defaultTokenFields,
  476. type: Token.ValueSize as const,
  477. value: Number(value),
  478. unit,
  479. });
  480. tokenValuePercentage = (value: string) => ({
  481. ...this.defaultTokenFields,
  482. type: Token.ValuePercentage as const,
  483. value: Number(value),
  484. });
  485. tokenValueBoolean = (value: string) => ({
  486. ...this.defaultTokenFields,
  487. type: Token.ValueBoolean as const,
  488. value: ['1', 'true'].includes(value.toLowerCase()),
  489. });
  490. tokenValueNumber = (value: string, unit: string) => ({
  491. ...this.defaultTokenFields,
  492. type: Token.ValueNumber as const,
  493. value,
  494. rawValue: Number(value) * (numberUnits[unit] ?? 1),
  495. unit,
  496. });
  497. tokenValueNumberList = (
  498. item1: ReturnType<TokenConverter['tokenValueNumber']>,
  499. items: ListItem<ReturnType<TokenConverter['tokenValueNumber']>>[]
  500. ) => ({
  501. ...this.defaultTokenFields,
  502. type: Token.ValueNumberList as const,
  503. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  504. });
  505. tokenValueTextList = (
  506. item1: ReturnType<TokenConverter['tokenValueText']>,
  507. items: ListItem<ReturnType<TokenConverter['tokenValueText']>>[]
  508. ) => ({
  509. ...this.defaultTokenFields,
  510. type: Token.ValueTextList as const,
  511. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  512. });
  513. tokenValueText = (value: string, quoted: boolean) => ({
  514. ...this.defaultTokenFields,
  515. type: Token.ValueText as const,
  516. value,
  517. quoted,
  518. });
  519. /**
  520. * This method is used while tokenizing to predicate whether a filter should
  521. * match or not. We do this because not all keys are valid for specific
  522. * filter types. For example, boolean filters should only match for keys
  523. * which can be filtered as booleans.
  524. *
  525. * See [0] and look for &{ predicate } to understand how predicates are
  526. * declared in the grammar
  527. *
  528. * [0]:https://pegjs.org/documentation
  529. */
  530. predicateFilter = <T extends FilterType>(type: T, key: FilterMap[T]['key']) => {
  531. // @ts-expect-error Unclear why this isn’t resolving correctly
  532. const keyName = getKeyName(key);
  533. const aggregateKey = key as ReturnType<TokenConverter['tokenKeyAggregate']>;
  534. const {isNumeric, isDuration, isBoolean, isDate, isPercentage, isSize} =
  535. this.keyValidation;
  536. const checkAggregate = (check: (s: string) => boolean) =>
  537. aggregateKey.args?.args.some(arg => check(arg?.value?.value ?? ''));
  538. switch (type) {
  539. case FilterType.Numeric:
  540. case FilterType.NumericIn:
  541. return isNumeric(keyName);
  542. case FilterType.Duration:
  543. return isDuration(keyName);
  544. case FilterType.Size:
  545. return isSize(keyName);
  546. case FilterType.Boolean:
  547. return isBoolean(keyName);
  548. case FilterType.Date:
  549. case FilterType.RelativeDate:
  550. case FilterType.SpecificDate:
  551. return isDate(keyName);
  552. case FilterType.AggregateDuration:
  553. return checkAggregate(isDuration);
  554. case FilterType.AggregateDate:
  555. return checkAggregate(isDate);
  556. case FilterType.AggregatePercentage:
  557. return checkAggregate(isPercentage);
  558. default:
  559. return true;
  560. }
  561. };
  562. /**
  563. * Predicates weather a text filter have operators for specific keys.
  564. */
  565. predicateTextOperator = (key: TextFilter['key']) =>
  566. this.config.textOperatorKeys.has(getKeyName(key));
  567. /**
  568. * Checks a filter against some non-grammar validation rules
  569. */
  570. checkInvalidFilter = <T extends FilterType>(
  571. filter: T,
  572. key: FilterMap[T]['key'],
  573. value: FilterMap[T]['value']
  574. ) => {
  575. // Text filter is the "fall through" filter that will match when other
  576. // filter predicates fail.
  577. if (filter === FilterType.Text) {
  578. return this.checkInvalidTextFilter(
  579. key as TextFilter['key'],
  580. value as TextFilter['value']
  581. );
  582. }
  583. if (filter === FilterType.Is || filter === FilterType.Has) {
  584. return this.checkInvalidTextValue(value as TextFilter['value']);
  585. }
  586. if ([FilterType.TextIn, FilterType.NumericIn].includes(filter)) {
  587. return this.checkInvalidInFilter(value as InFilter['value']);
  588. }
  589. return null;
  590. };
  591. /**
  592. * Validates text filters which may have failed predication
  593. */
  594. checkInvalidTextFilter = (key: TextFilter['key'], value: TextFilter['value']) => {
  595. if (
  596. this.config.validateKeys &&
  597. this.config.supportedTags &&
  598. !this.config.supportedTags[key.text]
  599. ) {
  600. return {reason: t('Invalid key. "%s" is not a supported search key.', key.text)};
  601. }
  602. // Explicit tag keys will always be treated as text filters
  603. if (key.type === Token.KeyExplicitTag) {
  604. return this.checkInvalidTextValue(value);
  605. }
  606. const keyName = getKeyName(key);
  607. if (this.keyValidation.isDuration(keyName)) {
  608. return {
  609. reason: t('Invalid duration. Expected number followed by duration unit suffix'),
  610. expectedType: [FilterType.Duration],
  611. };
  612. }
  613. if (this.keyValidation.isDate(keyName)) {
  614. const date = new Date();
  615. date.setSeconds(0);
  616. date.setMilliseconds(0);
  617. const example = date.toISOString();
  618. return {
  619. reason: t(
  620. 'Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. %s or %s)',
  621. example.slice(0, 10),
  622. example
  623. ),
  624. expectedType: [FilterType.Date, FilterType.SpecificDate, FilterType.RelativeDate],
  625. };
  626. }
  627. if (this.keyValidation.isBoolean(keyName)) {
  628. return {
  629. reason: t('Invalid boolean. Expected true, 1, false, or 0.'),
  630. expectedType: [FilterType.Boolean],
  631. };
  632. }
  633. if (this.keyValidation.isSize(keyName)) {
  634. return {
  635. reason: t('Invalid file size. Expected number followed by file size unit suffix'),
  636. expectedType: [FilterType.Size],
  637. };
  638. }
  639. if (this.keyValidation.isNumeric(keyName)) {
  640. return {
  641. reason: t(
  642. 'Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)'
  643. ),
  644. expectedType: [FilterType.Numeric, FilterType.NumericIn],
  645. };
  646. }
  647. return this.checkInvalidTextValue(value);
  648. };
  649. /**
  650. * Validates the value of a text filter
  651. */
  652. checkInvalidTextValue = (value: TextFilter['value']) => {
  653. if (!value.quoted && /(^|[^\\])"/.test(value.value)) {
  654. return {reason: t('Quotes must enclose text or be escaped')};
  655. }
  656. if (!value.quoted && value.value === '') {
  657. return {reason: t('Filter must have a value')};
  658. }
  659. return null;
  660. };
  661. /**
  662. * Validates IN filter values do not have an missing elements
  663. */
  664. checkInvalidInFilter = ({items}: InFilter['value']) => {
  665. const hasEmptyValue = items.some(item => item.value === null);
  666. if (hasEmptyValue) {
  667. return {reason: t('Lists should not have empty values')};
  668. }
  669. return null;
  670. };
  671. }
  672. /**
  673. * Maps token conversion methods to their result types
  674. */
  675. type ConverterResultMap = {
  676. [K in keyof TokenConverter & `token${string}`]: ReturnType<TokenConverter[K]>;
  677. };
  678. type Converter = keyof ConverterResultMap;
  679. /**
  680. * Converter keys specific to Key and Value tokens
  681. */
  682. type KVTokens = Converter & `token${'Key' | 'Value'}${string}`;
  683. /**
  684. * Similar to TokenResult, but only includes Key* and Value* token type
  685. * results. This avoids a circular reference when this is used for the Filter
  686. * token converter result
  687. */
  688. type KVConverter<T extends Token> = ConverterResultMap[KVTokens] & {type: T};
  689. /**
  690. * Each token type is discriminated by the `type` field.
  691. */
  692. export type TokenResult<T extends Token> = ConverterResultMap[Converter] & {type: T};
  693. /**
  694. * Result from parsing a search query.
  695. */
  696. export type ParseResult = Array<
  697. | TokenResult<Token.LogicBoolean>
  698. | TokenResult<Token.LogicGroup>
  699. | TokenResult<Token.Filter>
  700. | TokenResult<Token.FreeText>
  701. | TokenResult<Token.Spaces>
  702. >;
  703. /**
  704. * Configures behavior of search parsing
  705. */
  706. export type SearchConfig = {
  707. /**
  708. * Enables boolean filtering (AND / OR)
  709. */
  710. allowBoolean: boolean;
  711. /**
  712. * Keys considered valid for boolean filter types
  713. */
  714. booleanKeys: Set<string>;
  715. /**
  716. * Keys considered valid for date filter types
  717. */
  718. dateKeys: Set<string>;
  719. /**
  720. * Keys which are considered valid for duration filters
  721. */
  722. durationKeys: Set<string>;
  723. /**
  724. * Keys considered valid for numeric filter types
  725. */
  726. numericKeys: Set<string>;
  727. /**
  728. * Keys considered valid for the percentage aggregate and may have percentage
  729. * search values
  730. */
  731. percentageKeys: Set<string>;
  732. /**
  733. * Keys considered valid for size filter types
  734. */
  735. sizeKeys: Set<string>;
  736. /**
  737. * Text filter keys we allow to have operators
  738. */
  739. textOperatorKeys: Set<string>;
  740. /**
  741. * If validateKeys is set to true, tag keys that don't exist in supportedTags will be consider invalid
  742. */
  743. supportedTags?: TagCollection;
  744. /**
  745. * If set to true, tag keys that don't exist in supportedTags will be consider invalid
  746. */
  747. validateKeys?: boolean;
  748. };
  749. const defaultConfig: SearchConfig = {
  750. textOperatorKeys: new Set([
  751. 'release.version',
  752. 'release.build',
  753. 'release.package',
  754. 'release.stage',
  755. ]),
  756. durationKeys: new Set(['transaction.duration']),
  757. percentageKeys: new Set(['percentage']),
  758. // do not put functions in this Set
  759. numericKeys: new Set([
  760. 'project_id',
  761. 'project.id',
  762. 'issue.id',
  763. 'stack.colno',
  764. 'stack.lineno',
  765. 'stack.stack_level',
  766. 'transaction.duration',
  767. ]),
  768. dateKeys: new Set([
  769. 'start',
  770. 'end',
  771. 'firstSeen',
  772. 'lastSeen',
  773. 'last_seen()',
  774. 'time',
  775. 'event.timestamp',
  776. 'timestamp',
  777. 'timestamp.to_hour',
  778. 'timestamp.to_day',
  779. ]),
  780. booleanKeys: new Set([
  781. 'error.handled',
  782. 'error.unhandled',
  783. 'stack.in_app',
  784. 'team_key_transaction',
  785. ]),
  786. sizeKeys: new Set([]),
  787. allowBoolean: true,
  788. };
  789. const options = {
  790. TokenConverter,
  791. TermOperator,
  792. FilterType,
  793. config: defaultConfig,
  794. };
  795. /**
  796. * Parse a search query into a ParseResult. Failing to parse the search query
  797. * will result in null.
  798. */
  799. export function parseSearch(
  800. query: string,
  801. additionalConfig?: Partial<SearchConfig>
  802. ): ParseResult | null {
  803. // Merge additionalConfig with defaultConfig
  804. const config = additionalConfig
  805. ? {
  806. ...additionalConfig,
  807. ...Object.keys(defaultConfig).reduce((configAccumulator, key) => {
  808. configAccumulator[key] =
  809. typeof defaultConfig[key] === 'object'
  810. ? new Set([...defaultConfig[key], ...(additionalConfig[key] ?? [])])
  811. : defaultConfig[key];
  812. return configAccumulator;
  813. }, {}),
  814. }
  815. : defaultConfig;
  816. try {
  817. return grammar.parse(query, {...options, config});
  818. } catch (e) {
  819. // TODO(epurkhiser): Should we capture these errors somewhere?
  820. }
  821. return null;
  822. }
  823. /**
  824. * Join a parsed query array into a string.
  825. * Should handle null cases to chain easily with parseSearch.
  826. * Option to add a leading space when applicable (e.g. to combine with other strings).
  827. * Option to add a space between elements (e.g. for when no Token.Spaces present).
  828. */
  829. export function joinQuery(
  830. parsedTerms: ParseResult | null | undefined,
  831. leadingSpace?: boolean,
  832. additionalSpaceBetween?: boolean
  833. ): string {
  834. if (!parsedTerms || !parsedTerms.length) {
  835. return '';
  836. }
  837. return (
  838. (leadingSpace ? ' ' : '') +
  839. (parsedTerms.length === 1
  840. ? parsedTerms[0].text
  841. : parsedTerms.map(p => p.text).join(additionalSpaceBetween ? ' ' : ''))
  842. );
  843. }