fields.tsx 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  1. import isEqual from 'lodash/isEqual';
  2. import {RELEASE_ADOPTION_STAGES} from 'sentry/constants';
  3. import {MetricsType, Organization, SelectValue} from 'sentry/types';
  4. import {assert} from 'sentry/types/utils';
  5. import {
  6. SESSIONS_FIELDS,
  7. SESSIONS_OPERATIONS,
  8. } from 'sentry/views/dashboardsV2/widgetBuilder/releaseWidget/fields';
  9. import {
  10. AGGREGATION_FIELDS,
  11. AggregationKey,
  12. DISCOVER_FIELDS,
  13. FieldKey,
  14. FIELDS,
  15. FieldValueType,
  16. MEASUREMENT_FIELDS,
  17. WebVital,
  18. } from '../fields';
  19. export type Sort = {
  20. field: string;
  21. kind: 'asc' | 'desc';
  22. };
  23. // Contains the URL field value & the related table column width.
  24. // Can be parsed into a Column using explodeField()
  25. export type Field = {
  26. field: string;
  27. // When an alias is defined for a field, it will be shown as a column name in the table visualization.
  28. alias?: string;
  29. width?: number;
  30. };
  31. // ColumnType is kept as a string literal union instead of an enum due to the countless uses of it and refactoring would take huge effort.
  32. export type ColumnType = `${Exclude<FieldValueType, FieldValueType.NEVER>}`;
  33. export type ColumnValueType = ColumnType | `${FieldValueType.NEVER}`;
  34. export type ParsedFunction = {
  35. arguments: string[];
  36. name: string;
  37. };
  38. type ValidateColumnValueFunction = ({name: string, dataType: ColumnType}) => boolean;
  39. export type ValidateColumnTypes =
  40. | ColumnType[]
  41. | MetricsType[]
  42. | ValidateColumnValueFunction;
  43. export type AggregateParameter =
  44. | {
  45. columnTypes: Readonly<ValidateColumnTypes>;
  46. kind: 'column';
  47. required: boolean;
  48. defaultValue?: string;
  49. }
  50. | {
  51. dataType: ColumnType;
  52. kind: 'value';
  53. required: boolean;
  54. defaultValue?: string;
  55. placeholder?: string;
  56. }
  57. | {
  58. dataType: string;
  59. kind: 'dropdown';
  60. options: SelectValue<string>[];
  61. required: boolean;
  62. defaultValue?: string;
  63. placeholder?: string;
  64. };
  65. export type AggregationRefinement = string | undefined;
  66. // The parsed result of a Field.
  67. // Functions and Fields are handled as subtypes to enable other
  68. // code to work more simply.
  69. // This type can be converted into a Field.field using generateFieldAsString()
  70. // When an alias is defined for a field, it will be shown as a column name in the table visualization.
  71. export type QueryFieldValue =
  72. | {
  73. field: string;
  74. kind: 'field';
  75. alias?: string;
  76. }
  77. | {
  78. field: string;
  79. kind: 'calculatedField';
  80. alias?: string;
  81. }
  82. | {
  83. field: string;
  84. kind: 'equation';
  85. alias?: string;
  86. }
  87. | {
  88. function: [
  89. AggregationKeyWithAlias,
  90. string,
  91. AggregationRefinement,
  92. AggregationRefinement
  93. ];
  94. kind: 'function';
  95. alias?: string;
  96. };
  97. // Column is just an alias of a Query value
  98. export type Column = QueryFieldValue;
  99. export type Alignments = 'left' | 'right';
  100. const CONDITIONS_ARGUMENTS: SelectValue<string>[] = [
  101. {
  102. label: 'is equal to',
  103. value: 'equals',
  104. },
  105. {
  106. label: 'is not equal to',
  107. value: 'notEquals',
  108. },
  109. {
  110. label: 'is less than',
  111. value: 'less',
  112. },
  113. {
  114. label: 'is greater than',
  115. value: 'greater',
  116. },
  117. {
  118. label: 'is less than or equal to',
  119. value: 'lessOrEquals',
  120. },
  121. {
  122. label: 'is greater than or equal to',
  123. value: 'greaterOrEquals',
  124. },
  125. ];
  126. const WEB_VITALS_QUALITY: SelectValue<string>[] = [
  127. {
  128. label: 'good',
  129. value: 'good',
  130. },
  131. {
  132. label: 'meh',
  133. value: 'meh',
  134. },
  135. {
  136. label: 'poor',
  137. value: 'poor',
  138. },
  139. {
  140. label: 'any',
  141. value: 'any',
  142. },
  143. ];
  144. const getDocsAndOutputType = (key: AggregationKey) => {
  145. return {
  146. documentation: AGGREGATION_FIELDS[key].desc,
  147. outputType: AGGREGATION_FIELDS[key].valueType as AggregationOutputType,
  148. };
  149. };
  150. // Refer to src/sentry/search/events/fields.py
  151. // Try to keep functions logically sorted, ie. all the count functions are grouped together
  152. export const AGGREGATIONS = {
  153. [AggregationKey.Count]: {
  154. ...getDocsAndOutputType(AggregationKey.Count),
  155. parameters: [],
  156. isSortable: true,
  157. multiPlotType: 'area',
  158. },
  159. [AggregationKey.CountUnique]: {
  160. ...getDocsAndOutputType(AggregationKey.CountUnique),
  161. parameters: [
  162. {
  163. kind: 'column',
  164. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  165. defaultValue: 'user',
  166. required: true,
  167. },
  168. ],
  169. isSortable: true,
  170. multiPlotType: 'line',
  171. },
  172. [AggregationKey.CountMiserable]: {
  173. ...getDocsAndOutputType(AggregationKey.CountMiserable),
  174. getFieldOverrides({parameter}: DefaultValueInputs) {
  175. if (parameter.kind === 'column') {
  176. return {defaultValue: 'user'};
  177. }
  178. return {
  179. defaultValue: parameter.defaultValue,
  180. };
  181. },
  182. parameters: [
  183. {
  184. kind: 'column',
  185. columnTypes: validateAllowedColumns(['user']),
  186. defaultValue: 'user',
  187. required: true,
  188. },
  189. {
  190. kind: 'value',
  191. dataType: 'number',
  192. defaultValue: '300',
  193. required: true,
  194. },
  195. ],
  196. isSortable: true,
  197. multiPlotType: 'area',
  198. },
  199. [AggregationKey.CountIf]: {
  200. ...getDocsAndOutputType(AggregationKey.CountIf),
  201. parameters: [
  202. {
  203. kind: 'column',
  204. columnTypes: validateDenyListColumns(
  205. ['string', 'duration', 'number'],
  206. ['id', 'issue', 'user.display']
  207. ),
  208. defaultValue: 'transaction.duration',
  209. required: true,
  210. },
  211. {
  212. kind: 'dropdown',
  213. options: CONDITIONS_ARGUMENTS,
  214. dataType: 'string',
  215. defaultValue: CONDITIONS_ARGUMENTS[0].value,
  216. required: true,
  217. },
  218. {
  219. kind: 'value',
  220. dataType: 'string',
  221. defaultValue: '300',
  222. required: true,
  223. },
  224. ],
  225. isSortable: true,
  226. multiPlotType: 'area',
  227. },
  228. [AggregationKey.CountWebVitals]: {
  229. ...getDocsAndOutputType(AggregationKey.CountWebVitals),
  230. parameters: [
  231. {
  232. kind: 'column',
  233. columnTypes: validateAllowedColumns([
  234. WebVital.LCP,
  235. WebVital.FP,
  236. WebVital.FCP,
  237. WebVital.FID,
  238. WebVital.CLS,
  239. ]),
  240. defaultValue: WebVital.LCP,
  241. required: true,
  242. },
  243. {
  244. kind: 'dropdown',
  245. options: WEB_VITALS_QUALITY,
  246. dataType: 'string',
  247. defaultValue: WEB_VITALS_QUALITY[0].value,
  248. required: true,
  249. },
  250. ],
  251. isSortable: true,
  252. multiPlotType: 'area',
  253. },
  254. [AggregationKey.Eps]: {
  255. ...getDocsAndOutputType(AggregationKey.Eps),
  256. parameters: [],
  257. isSortable: true,
  258. multiPlotType: 'area',
  259. },
  260. [AggregationKey.Epm]: {
  261. ...getDocsAndOutputType(AggregationKey.Epm),
  262. parameters: [],
  263. isSortable: true,
  264. multiPlotType: 'area',
  265. },
  266. [AggregationKey.FailureCount]: {
  267. ...getDocsAndOutputType(AggregationKey.FailureCount),
  268. parameters: [],
  269. isSortable: true,
  270. multiPlotType: 'line',
  271. },
  272. [AggregationKey.Min]: {
  273. ...getDocsAndOutputType(AggregationKey.Min),
  274. parameters: [
  275. {
  276. kind: 'column',
  277. columnTypes: validateForNumericAggregate([
  278. 'integer',
  279. 'number',
  280. 'duration',
  281. 'date',
  282. 'percentage',
  283. ]),
  284. defaultValue: 'transaction.duration',
  285. required: true,
  286. },
  287. ],
  288. isSortable: true,
  289. multiPlotType: 'line',
  290. },
  291. [AggregationKey.Max]: {
  292. ...getDocsAndOutputType(AggregationKey.Max),
  293. parameters: [
  294. {
  295. kind: 'column',
  296. columnTypes: validateForNumericAggregate([
  297. 'integer',
  298. 'number',
  299. 'duration',
  300. 'date',
  301. 'percentage',
  302. ]),
  303. defaultValue: 'transaction.duration',
  304. required: true,
  305. },
  306. ],
  307. isSortable: true,
  308. multiPlotType: 'line',
  309. },
  310. [AggregationKey.Sum]: {
  311. ...getDocsAndOutputType(AggregationKey.Sum),
  312. parameters: [
  313. {
  314. kind: 'column',
  315. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  316. required: true,
  317. defaultValue: 'transaction.duration',
  318. },
  319. ],
  320. isSortable: true,
  321. multiPlotType: 'area',
  322. },
  323. [AggregationKey.Any]: {
  324. ...getDocsAndOutputType(AggregationKey.Any),
  325. parameters: [
  326. {
  327. kind: 'column',
  328. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  329. required: true,
  330. defaultValue: 'transaction.duration',
  331. },
  332. ],
  333. isSortable: true,
  334. },
  335. [AggregationKey.P50]: {
  336. ...getDocsAndOutputType(AggregationKey.P50),
  337. parameters: [
  338. {
  339. kind: 'column',
  340. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  341. defaultValue: 'transaction.duration',
  342. required: false,
  343. },
  344. ],
  345. isSortable: true,
  346. multiPlotType: 'line',
  347. },
  348. [AggregationKey.P75]: {
  349. ...getDocsAndOutputType(AggregationKey.P75),
  350. parameters: [
  351. {
  352. kind: 'column',
  353. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  354. defaultValue: 'transaction.duration',
  355. required: false,
  356. },
  357. ],
  358. isSortable: true,
  359. multiPlotType: 'line',
  360. },
  361. [AggregationKey.P95]: {
  362. ...getDocsAndOutputType(AggregationKey.P95),
  363. parameters: [
  364. {
  365. kind: 'column',
  366. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  367. defaultValue: 'transaction.duration',
  368. required: false,
  369. },
  370. ],
  371. type: [],
  372. isSortable: true,
  373. multiPlotType: 'line',
  374. },
  375. [AggregationKey.P99]: {
  376. ...getDocsAndOutputType(AggregationKey.P99),
  377. parameters: [
  378. {
  379. kind: 'column',
  380. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  381. defaultValue: 'transaction.duration',
  382. required: false,
  383. },
  384. ],
  385. isSortable: true,
  386. multiPlotType: 'line',
  387. },
  388. [AggregationKey.P100]: {
  389. ...getDocsAndOutputType(AggregationKey.P100),
  390. parameters: [
  391. {
  392. kind: 'column',
  393. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  394. defaultValue: 'transaction.duration',
  395. required: false,
  396. },
  397. ],
  398. isSortable: true,
  399. multiPlotType: 'line',
  400. },
  401. [AggregationKey.Percentile]: {
  402. ...getDocsAndOutputType(AggregationKey.Percentile),
  403. parameters: [
  404. {
  405. kind: 'column',
  406. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  407. defaultValue: 'transaction.duration',
  408. required: true,
  409. },
  410. {
  411. kind: 'value',
  412. dataType: 'number',
  413. defaultValue: '0.5',
  414. required: true,
  415. },
  416. ],
  417. isSortable: true,
  418. multiPlotType: 'line',
  419. },
  420. [AggregationKey.Avg]: {
  421. ...getDocsAndOutputType(AggregationKey.Avg),
  422. parameters: [
  423. {
  424. kind: 'column',
  425. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  426. defaultValue: 'transaction.duration',
  427. required: true,
  428. },
  429. ],
  430. isSortable: true,
  431. multiPlotType: 'line',
  432. },
  433. [AggregationKey.Apdex]: {
  434. ...getDocsAndOutputType(AggregationKey.Apdex),
  435. parameters: [
  436. {
  437. kind: 'value',
  438. dataType: 'number',
  439. defaultValue: '300',
  440. required: true,
  441. },
  442. ],
  443. isSortable: true,
  444. multiPlotType: 'line',
  445. },
  446. [AggregationKey.UserMisery]: {
  447. ...getDocsAndOutputType(AggregationKey.UserMisery),
  448. parameters: [
  449. {
  450. kind: 'value',
  451. dataType: 'number',
  452. defaultValue: '300',
  453. required: true,
  454. },
  455. ],
  456. isSortable: true,
  457. multiPlotType: 'line',
  458. },
  459. [AggregationKey.FailureRate]: {
  460. ...getDocsAndOutputType(AggregationKey.FailureRate),
  461. parameters: [],
  462. isSortable: true,
  463. multiPlotType: 'line',
  464. },
  465. [AggregationKey.LastSeen]: {
  466. ...getDocsAndOutputType(AggregationKey.LastSeen),
  467. parameters: [],
  468. isSortable: true,
  469. },
  470. } as const;
  471. // TPM and TPS are aliases that are only used in Performance
  472. export const ALIASES = {
  473. tpm: AggregationKey.Epm,
  474. tps: AggregationKey.Eps,
  475. };
  476. assert(AGGREGATIONS as Readonly<{[key in AggregationKey]: Aggregation}>);
  477. export type AggregationKeyWithAlias = `${AggregationKey}` | keyof typeof ALIASES | '';
  478. export type AggregationOutputType = Extract<
  479. ColumnType,
  480. 'number' | 'integer' | 'date' | 'duration' | 'percentage' | 'string'
  481. >;
  482. export type PlotType = 'bar' | 'line' | 'area';
  483. type DefaultValueInputs = {
  484. parameter: AggregateParameter;
  485. };
  486. export type Aggregation = {
  487. /**
  488. * Can this function be used in a sort result
  489. */
  490. isSortable: boolean;
  491. /**
  492. * The output type. Null means to inherit from the field.
  493. */
  494. outputType: AggregationOutputType | null;
  495. /**
  496. * List of parameters for the function.
  497. */
  498. parameters: Readonly<AggregateParameter[]>;
  499. getFieldOverrides?: (
  500. data: DefaultValueInputs
  501. ) => Partial<Omit<AggregateParameter, 'kind'>>;
  502. /**
  503. * How this function should be plotted when shown in a multiseries result (top5)
  504. * Optional because some functions cannot be plotted (strings/dates)
  505. */
  506. multiPlotType?: PlotType;
  507. };
  508. export const DEPRECATED_FIELDS: string[] = [FieldKey.CULPRIT];
  509. export type FieldTag = {
  510. key: FieldKey;
  511. name: FieldKey;
  512. };
  513. export const FIELD_TAGS = Object.freeze(
  514. Object.fromEntries(DISCOVER_FIELDS.map(item => [item, {key: item, name: item}]))
  515. );
  516. export const SEMVER_TAGS = {
  517. [FieldKey.RELEASE_VERSION]: {
  518. key: FieldKey.RELEASE_VERSION,
  519. name: FieldKey.RELEASE_VERSION,
  520. },
  521. [FieldKey.RELEASE_BUILD]: {
  522. key: FieldKey.RELEASE_BUILD,
  523. name: FieldKey.RELEASE_BUILD,
  524. },
  525. [FieldKey.RELEASE_PACKAGE]: {
  526. key: FieldKey.RELEASE_PACKAGE,
  527. name: FieldKey.RELEASE_PACKAGE,
  528. },
  529. [FieldKey.RELEASE_STAGE]: {
  530. key: FieldKey.RELEASE_STAGE,
  531. name: FieldKey.RELEASE_STAGE,
  532. predefined: true,
  533. values: RELEASE_ADOPTION_STAGES,
  534. },
  535. };
  536. /**
  537. * Some tag keys should never be formatted as `tag[...]`
  538. * when used as a filter because they are predefined.
  539. */
  540. const EXCLUDED_TAG_KEYS = new Set(['release']);
  541. export function formatTagKey(key: string): string {
  542. // Some tags may be normalized from context, but not all of them are.
  543. // This supports a user making a custom tag with the same name as one
  544. // that comes from context as all of these are also tags.
  545. if (key in FIELD_TAGS && !EXCLUDED_TAG_KEYS.has(key)) {
  546. return `tags[${key}]`;
  547. }
  548. return key;
  549. }
  550. // Allows for a less strict field key definition in cases we are returning custom strings as fields
  551. export type LooseFieldKey = FieldKey | string | '';
  552. export type MeasurementType =
  553. | FieldValueType.DURATION
  554. | FieldValueType.NUMBER
  555. | FieldValueType.INTEGER
  556. | FieldValueType.PERCENTAGE;
  557. export function isSpanOperationBreakdownField(field: string) {
  558. return field.startsWith('spans.');
  559. }
  560. export const SPAN_OP_RELATIVE_BREAKDOWN_FIELD = 'span_ops_breakdown.relative';
  561. export function isRelativeSpanOperationBreakdownField(field: string) {
  562. return field === SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  563. }
  564. export const SPAN_OP_BREAKDOWN_FIELDS = [
  565. 'spans.http',
  566. 'spans.db',
  567. 'spans.browser',
  568. 'spans.resource',
  569. 'spans.ui',
  570. ];
  571. // This list contains fields/functions that are available with performance-view feature.
  572. export const TRACING_FIELDS = [
  573. AggregationKey.Avg,
  574. AggregationKey.Sum,
  575. FieldKey.TRANSACTION_DURATION,
  576. FieldKey.TRANSACTION_OP,
  577. FieldKey.TRANSACTION_STATUS,
  578. AggregationKey.P50,
  579. AggregationKey.P75,
  580. AggregationKey.P95,
  581. AggregationKey.P99,
  582. AggregationKey.P100,
  583. AggregationKey.Percentile,
  584. AggregationKey.FailureRate,
  585. AggregationKey.Apdex,
  586. AggregationKey.CountMiserable,
  587. AggregationKey.UserMisery,
  588. AggregationKey.Eps,
  589. AggregationKey.Epm,
  590. 'team_key_transaction',
  591. ...Object.keys(MEASUREMENT_FIELDS),
  592. ...SPAN_OP_BREAKDOWN_FIELDS,
  593. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  594. ];
  595. export const MEASUREMENT_PATTERN = /^measurements\.([a-zA-Z0-9-_.]+)$/;
  596. export const SPAN_OP_BREAKDOWN_PATTERN = /^spans\.([a-zA-Z0-9-_.]+)$/;
  597. export function isMeasurement(field: string): boolean {
  598. const results = field.match(MEASUREMENT_PATTERN);
  599. return !!results;
  600. }
  601. export function measurementType(field: string): MeasurementType {
  602. if (MEASUREMENT_FIELDS.hasOwnProperty(field)) {
  603. return MEASUREMENT_FIELDS[field].valueType as MeasurementType;
  604. }
  605. return FieldValueType.NUMBER;
  606. }
  607. export function getMeasurementSlug(field: string): string | null {
  608. const results = field.match(MEASUREMENT_PATTERN);
  609. if (results && results.length >= 2) {
  610. return results[1];
  611. }
  612. return null;
  613. }
  614. const AGGREGATE_PATTERN = /^(\w+)\((.*)?\)$/;
  615. // Identical to AGGREGATE_PATTERN, but without the $ for newline, or ^ for start of line
  616. const AGGREGATE_BASE = /(\w+)\((.*)?\)/g;
  617. export function getAggregateArg(field: string): string | null {
  618. // only returns the first argument if field is an aggregate
  619. const result = parseFunction(field);
  620. if (result && result.arguments.length > 0) {
  621. return result.arguments[0];
  622. }
  623. return null;
  624. }
  625. export function parseFunction(field: string): ParsedFunction | null {
  626. const results = field.match(AGGREGATE_PATTERN);
  627. if (results && results.length === 3) {
  628. return {
  629. name: results[1],
  630. arguments: parseArguments(results[1], results[2]),
  631. };
  632. }
  633. return null;
  634. }
  635. export function parseArguments(functionText: string, columnText: string): string[] {
  636. // Some functions take a quoted string for their arguments that may contain commas
  637. // This function attempts to be identical with the similarly named parse_arguments
  638. // found in src/sentry/search/events/fields.py
  639. if (
  640. (functionText !== 'to_other' &&
  641. functionText !== 'count_if' &&
  642. functionText !== 'spans_histogram') ||
  643. columnText.length === 0
  644. ) {
  645. return columnText ? columnText.split(',').map(result => result.trim()) : [];
  646. }
  647. const args: string[] = [];
  648. let quoted = false;
  649. let escaped = false;
  650. let i: number = 0;
  651. let j: number = 0;
  652. while (j < columnText.length) {
  653. if (i === j && columnText[j] === '"') {
  654. // when we see a quote at the beginning of
  655. // an argument, then this is a quoted string
  656. quoted = true;
  657. } else if (i === j && columnText[j] === ' ') {
  658. // argument has leading spaces, skip over them
  659. i += 1;
  660. } else if (quoted && !escaped && columnText[j] === '\\') {
  661. // when we see a slash inside a quoted string,
  662. // the next character is an escape character
  663. escaped = true;
  664. } else if (quoted && !escaped && columnText[j] === '"') {
  665. // when we see a non-escaped quote while inside
  666. // of a quoted string, we should end it
  667. quoted = false;
  668. } else if (quoted && escaped) {
  669. // when we are inside a quoted string and have
  670. // begun an escape character, we should end it
  671. escaped = false;
  672. } else if (quoted && columnText[j] === ',') {
  673. // when we are inside a quoted string and see
  674. // a comma, it should not be considered an
  675. // argument separator
  676. } else if (columnText[j] === ',') {
  677. // when we see a comma outside of a quoted string
  678. // it is an argument separator
  679. args.push(columnText.substring(i, j).trim());
  680. i = j + 1;
  681. }
  682. j += 1;
  683. }
  684. if (i !== j) {
  685. // add in the last argument if any
  686. args.push(columnText.substring(i).trim());
  687. }
  688. return args;
  689. }
  690. // `|` is an invalid field character, so it is used to determine whether a field is an equation or not
  691. export const EQUATION_PREFIX = 'equation|';
  692. const EQUATION_ALIAS_PATTERN = /^equation\[(\d+)\]$/;
  693. export const CALCULATED_FIELD_PREFIX = 'calculated|';
  694. export function isEquation(field: string): boolean {
  695. return field.startsWith(EQUATION_PREFIX);
  696. }
  697. export function isEquationAlias(field: string): boolean {
  698. return EQUATION_ALIAS_PATTERN.test(field);
  699. }
  700. export function maybeEquationAlias(field: string): boolean {
  701. return field.includes(EQUATION_PREFIX);
  702. }
  703. export function stripEquationPrefix(field: string): string {
  704. return field.replace(EQUATION_PREFIX, '');
  705. }
  706. export function getEquationAliasIndex(field: string): number {
  707. const results = field.match(EQUATION_ALIAS_PATTERN);
  708. if (results && results.length === 2) {
  709. return parseInt(results[1], 10);
  710. }
  711. return -1;
  712. }
  713. export function getEquation(field: string): string {
  714. return field.slice(EQUATION_PREFIX.length);
  715. }
  716. export function isAggregateEquation(field: string): boolean {
  717. const results = field.match(AGGREGATE_BASE);
  718. return isEquation(field) && results !== null && results.length > 0;
  719. }
  720. export function isLegalEquationColumn(column: Column): boolean {
  721. // Any isn't allowed in arithmetic
  722. if (column.kind === 'function' && column.function[0] === 'any') {
  723. return false;
  724. }
  725. const columnType = getColumnType(column);
  726. return columnType === 'number' || columnType === 'integer' || columnType === 'duration';
  727. }
  728. export function generateAggregateFields(
  729. organization: Organization,
  730. eventFields: readonly Field[] | Field[],
  731. excludeFields: readonly string[] = []
  732. ): Field[] {
  733. const functions = Object.keys(AGGREGATIONS);
  734. const fields = Object.values(eventFields).map(field => field.field);
  735. functions.forEach(func => {
  736. const parameters = AGGREGATIONS[func].parameters.map(param => {
  737. const overrides = AGGREGATIONS[func].getFieldOverrides;
  738. if (typeof overrides === 'undefined') {
  739. return param;
  740. }
  741. return {
  742. ...param,
  743. ...overrides({parameter: param, organization}),
  744. };
  745. });
  746. if (parameters.every(param => typeof param.defaultValue !== 'undefined')) {
  747. const newField = `${func}(${parameters
  748. .map(param => param.defaultValue)
  749. .join(',')})`;
  750. if (fields.indexOf(newField) === -1 && excludeFields.indexOf(newField) === -1) {
  751. fields.push(newField);
  752. }
  753. }
  754. });
  755. return fields.map(field => ({field})) as Field[];
  756. }
  757. export function isDerivedMetric(field: string): boolean {
  758. return field.startsWith(CALCULATED_FIELD_PREFIX);
  759. }
  760. export function stripDerivedMetricsPrefix(field: string): string {
  761. return field.replace(CALCULATED_FIELD_PREFIX, '');
  762. }
  763. export function explodeFieldString(field: string, alias?: string): Column {
  764. if (isEquation(field)) {
  765. return {kind: 'equation', field: getEquation(field), alias};
  766. }
  767. if (isDerivedMetric(field)) {
  768. return {kind: 'calculatedField', field: stripDerivedMetricsPrefix(field), alias};
  769. }
  770. const results = parseFunction(field);
  771. if (results) {
  772. return {
  773. kind: 'function',
  774. function: [
  775. results.name as AggregationKey,
  776. results.arguments[0] ?? '',
  777. results.arguments[1] as AggregationRefinement,
  778. results.arguments[2] as AggregationRefinement,
  779. ],
  780. alias,
  781. };
  782. }
  783. return {kind: 'field', field, alias};
  784. }
  785. export function generateFieldAsString(value: QueryFieldValue): string {
  786. if (value.kind === 'field') {
  787. return value.field;
  788. }
  789. if (value.kind === 'calculatedField') {
  790. return `${CALCULATED_FIELD_PREFIX}${value.field}`;
  791. }
  792. if (value.kind === 'equation') {
  793. return `${EQUATION_PREFIX}${value.field}`;
  794. }
  795. const aggregation = value.function[0];
  796. const parameters = value.function.slice(1).filter(i => i);
  797. return `${aggregation}(${parameters.join(',')})`;
  798. }
  799. export function explodeField(field: Field): Column {
  800. return explodeFieldString(field.field, field.alias);
  801. }
  802. /**
  803. * Get the alias that the API results will have for a given aggregate function name
  804. */
  805. export function getAggregateAlias(field: string): string {
  806. const result = parseFunction(field);
  807. if (!result) {
  808. return field;
  809. }
  810. let alias = result.name;
  811. if (result.arguments.length > 0) {
  812. alias += '_' + result.arguments.join('_');
  813. }
  814. return alias.replace(/[^\w]/g, '_').replace(/^_+/g, '').replace(/_+$/, '');
  815. }
  816. /**
  817. * Check if a field name looks like an aggregate function or known aggregate alias.
  818. */
  819. export function isAggregateField(field: string): boolean {
  820. return parseFunction(field) !== null;
  821. }
  822. export function isAggregateFieldOrEquation(field: string): boolean {
  823. return isAggregateField(field) || isAggregateEquation(field) || isNumericMetrics(field);
  824. }
  825. /**
  826. * Temporary hardcoded hack to enable testing derived metrics.
  827. * Can be removed after we get rid of getAggregateFields
  828. */
  829. export function isNumericMetrics(field: string): boolean {
  830. return [
  831. 'session.crash_free_rate',
  832. 'session.crashed',
  833. 'session.errored_preaggregated',
  834. 'session.errored_set',
  835. 'session.init',
  836. ].includes(field);
  837. }
  838. export function getAggregateFields(fields: string[]): string[] {
  839. return fields.filter(
  840. field =>
  841. isAggregateField(field) || isAggregateEquation(field) || isNumericMetrics(field)
  842. );
  843. }
  844. export function getColumnsAndAggregates(fields: string[]): {
  845. aggregates: string[];
  846. columns: string[];
  847. } {
  848. const aggregates = getAggregateFields(fields);
  849. const columns = fields.filter(field => !!!aggregates.includes(field));
  850. return {columns, aggregates};
  851. }
  852. export function getColumnsAndAggregatesAsStrings(fields: QueryFieldValue[]): {
  853. aggregates: string[];
  854. columns: string[];
  855. fieldAliases: string[];
  856. } {
  857. // TODO(dam): distinguish between metrics, derived metrics and tags
  858. const aggregateFields: string[] = [];
  859. const nonAggregateFields: string[] = [];
  860. const fieldAliases: string[] = [];
  861. for (const field of fields) {
  862. const fieldString = generateFieldAsString(field);
  863. if (field.kind === 'function' || field.kind === 'calculatedField') {
  864. aggregateFields.push(fieldString);
  865. } else if (field.kind === 'equation') {
  866. if (isAggregateEquation(fieldString)) {
  867. aggregateFields.push(fieldString);
  868. } else {
  869. nonAggregateFields.push(fieldString);
  870. }
  871. } else {
  872. nonAggregateFields.push(fieldString);
  873. }
  874. fieldAliases.push(field.alias ?? '');
  875. }
  876. return {aggregates: aggregateFields, columns: nonAggregateFields, fieldAliases};
  877. }
  878. /**
  879. * Convert a function string into type it will output.
  880. * This is useful when you need to format values in tooltips,
  881. * or in series markers.
  882. */
  883. export function aggregateOutputType(field: string): AggregationOutputType {
  884. const result = parseFunction(field);
  885. if (!result) {
  886. return 'number';
  887. }
  888. const outputType = aggregateFunctionOutputType(result.name, result.arguments[0]);
  889. if (outputType === null) {
  890. return 'number';
  891. }
  892. return outputType;
  893. }
  894. /**
  895. * Converts a function string and its first argument into its output type.
  896. * - If the function has a fixed output type, that will be the result.
  897. * - If the function does not define an output type, the output type will be equal to
  898. * the type of its first argument.
  899. * - If the function has an optional first argument, and it was not defined, make sure
  900. * to use the default argument as the first argument.
  901. * - If the type could not be determined, return null.
  902. */
  903. export function aggregateFunctionOutputType(
  904. funcName: string,
  905. firstArg: string | undefined
  906. ): AggregationOutputType | null {
  907. const aggregate =
  908. AGGREGATIONS[ALIASES[funcName] || funcName] ?? SESSIONS_OPERATIONS[funcName];
  909. // Attempt to use the function's outputType.
  910. if (aggregate?.outputType) {
  911. return aggregate.outputType;
  912. }
  913. // If the first argument is undefined and it is not required,
  914. // then we attempt to get the default value.
  915. if (!firstArg && aggregate?.parameters?.[0]) {
  916. if (aggregate.parameters[0].required === false) {
  917. firstArg = aggregate.parameters[0].defaultValue;
  918. }
  919. }
  920. if (firstArg && SESSIONS_FIELDS.hasOwnProperty(firstArg)) {
  921. return SESSIONS_FIELDS[firstArg].type as AggregationOutputType;
  922. }
  923. // If the function is an inherit type it will have a field as
  924. // the first parameter and we can use that to get the type.
  925. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  926. return FIELDS[firstArg].valueType as AggregationOutputType;
  927. }
  928. if (firstArg && isMeasurement(firstArg)) {
  929. return measurementType(firstArg);
  930. }
  931. if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  932. return 'duration';
  933. }
  934. return null;
  935. }
  936. export function errorsAndTransactionsAggregateFunctionOutputType(
  937. funcName: string,
  938. firstArg: string | undefined
  939. ): AggregationOutputType | null {
  940. const aggregate = AGGREGATIONS[ALIASES[funcName] || funcName];
  941. // Attempt to use the function's outputType.
  942. if (aggregate?.outputType) {
  943. return aggregate.outputType;
  944. }
  945. // If the first argument is undefined and it is not required,
  946. // then we attempt to get the default value.
  947. if (!firstArg && aggregate?.parameters?.[0]) {
  948. if (aggregate.parameters[0].required === false) {
  949. firstArg = aggregate.parameters[0].defaultValue;
  950. }
  951. }
  952. // If the function is an inherit type it will have a field as
  953. // the first parameter and we can use that to get the type.
  954. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  955. return FIELDS[firstArg].valueType as AggregationOutputType;
  956. }
  957. if (firstArg && isMeasurement(firstArg)) {
  958. return measurementType(firstArg);
  959. }
  960. if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  961. return 'duration';
  962. }
  963. return null;
  964. }
  965. export function sessionsAggregateFunctionOutputType(
  966. funcName: string,
  967. firstArg: string | undefined
  968. ): AggregationOutputType | null {
  969. const aggregate = SESSIONS_OPERATIONS[funcName];
  970. // Attempt to use the function's outputType.
  971. if (aggregate?.outputType) {
  972. return aggregate.outputType;
  973. }
  974. // If the first argument is undefined and it is not required,
  975. // then we attempt to get the default value.
  976. if (!firstArg && aggregate?.parameters?.[0]) {
  977. if (aggregate.parameters[0].required === false) {
  978. firstArg = aggregate.parameters[0].defaultValue;
  979. }
  980. }
  981. if (firstArg && SESSIONS_FIELDS.hasOwnProperty(firstArg)) {
  982. return SESSIONS_FIELDS[firstArg].type as AggregationOutputType;
  983. }
  984. return null;
  985. }
  986. /**
  987. * Get the multi-series chart type for an aggregate function.
  988. */
  989. export function aggregateMultiPlotType(field: string): PlotType {
  990. if (isEquation(field)) {
  991. return 'line';
  992. }
  993. const result = parseFunction(field);
  994. // Handle invalid data.
  995. if (!result) {
  996. return 'area';
  997. }
  998. if (!AGGREGATIONS.hasOwnProperty(result.name)) {
  999. return 'area';
  1000. }
  1001. return AGGREGATIONS[result.name].multiPlotType;
  1002. }
  1003. function validateForNumericAggregate(
  1004. validColumnTypes: ColumnType[]
  1005. ): ValidateColumnValueFunction {
  1006. return function ({name, dataType}: {dataType: ColumnType; name: string}): boolean {
  1007. // these built-in columns cannot be applied to numeric aggregates such as percentile(...)
  1008. if (
  1009. [
  1010. FieldKey.DEVICE_BATTERY_LEVEL,
  1011. FieldKey.STACK_COLNO,
  1012. FieldKey.STACK_LINENO,
  1013. FieldKey.STACK_STACK_LEVEL,
  1014. ].includes(name as FieldKey)
  1015. ) {
  1016. return false;
  1017. }
  1018. return validColumnTypes.includes(dataType);
  1019. };
  1020. }
  1021. function validateDenyListColumns(
  1022. validColumnTypes: ColumnType[],
  1023. deniedColumns: string[]
  1024. ): ValidateColumnValueFunction {
  1025. return function ({name, dataType}: {dataType: ColumnType; name: string}): boolean {
  1026. return validColumnTypes.includes(dataType) && !deniedColumns.includes(name);
  1027. };
  1028. }
  1029. function validateAllowedColumns(validColumns: string[]): ValidateColumnValueFunction {
  1030. return function ({name}): boolean {
  1031. return validColumns.includes(name);
  1032. };
  1033. }
  1034. const alignedTypes: ColumnValueType[] = ['number', 'duration', 'integer', 'percentage'];
  1035. export function fieldAlignment(
  1036. columnName: string,
  1037. columnType?: undefined | ColumnValueType,
  1038. metadata?: Record<string, ColumnValueType>
  1039. ): Alignments {
  1040. let align: Alignments = 'left';
  1041. if (columnType) {
  1042. align = alignedTypes.includes(columnType) ? 'right' : 'left';
  1043. }
  1044. if (columnType === undefined || columnType === 'never') {
  1045. // fallback to align the column based on the table metadata
  1046. const maybeType = metadata ? metadata[getAggregateAlias(columnName)] : undefined;
  1047. if (maybeType !== undefined && alignedTypes.includes(maybeType)) {
  1048. align = 'right';
  1049. }
  1050. }
  1051. return align;
  1052. }
  1053. /**
  1054. * Match on types that are legal to show on a timeseries chart.
  1055. */
  1056. export function isLegalYAxisType(match: ColumnType | MetricsType) {
  1057. return ['number', 'integer', 'duration', 'percentage'].includes(match);
  1058. }
  1059. export function getSpanOperationName(field: string): string | null {
  1060. const results = field.match(SPAN_OP_BREAKDOWN_PATTERN);
  1061. if (results && results.length >= 2) {
  1062. return results[1];
  1063. }
  1064. return null;
  1065. }
  1066. export function getColumnType(column: Column): ColumnType {
  1067. if (column.kind === 'function') {
  1068. const outputType = aggregateFunctionOutputType(
  1069. column.function[0],
  1070. column.function[1]
  1071. );
  1072. if (outputType !== null) {
  1073. return outputType;
  1074. }
  1075. } else if (column.kind === 'field') {
  1076. if (FIELDS.hasOwnProperty(column.field)) {
  1077. return FIELDS[column.field].valueType as ColumnType;
  1078. }
  1079. if (isMeasurement(column.field)) {
  1080. return measurementType(column.field);
  1081. }
  1082. if (isSpanOperationBreakdownField(column.field)) {
  1083. return 'duration';
  1084. }
  1085. }
  1086. return 'string';
  1087. }
  1088. export function hasDuplicate(columnList: Column[], column: Column): boolean {
  1089. if (column.kind !== 'function' && column.kind !== 'field') {
  1090. return false;
  1091. }
  1092. return columnList.filter(newColumn => isEqual(newColumn, column)).length > 1;
  1093. }