parser.tsx 21 KB

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