fields.tsx 36 KB

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