fields.tsx 34 KB

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