fields.tsx 35 KB

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