parser.tsx 20 KB

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