fields.tsx 33 KB

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