fields.tsx 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519
  1. import isEqual from 'lodash/isEqual';
  2. import {RELEASE_ADOPTION_STAGES} from 'sentry/constants';
  3. import {t} from 'sentry/locale';
  4. import {MetricsType, Organization, SelectValue} from 'sentry/types';
  5. import {assert} from 'sentry/types/utils';
  6. import {
  7. SESSIONS_FIELDS,
  8. SESSIONS_OPERATIONS,
  9. } from 'sentry/views/dashboardsV2/widgetBuilder/releaseWidget/fields';
  10. export type Sort = {
  11. field: string;
  12. kind: 'asc' | 'desc';
  13. };
  14. // Contains the URL field value & the related table column width.
  15. // Can be parsed into a Column using explodeField()
  16. export type Field = {
  17. field: string;
  18. // When an alias is defined for a field, it will be shown as a column name in the table visualization.
  19. alias?: string;
  20. width?: number;
  21. };
  22. export type ColumnType =
  23. | 'boolean'
  24. | 'date'
  25. | 'duration'
  26. | 'integer'
  27. | 'number'
  28. | 'percentage'
  29. | 'string';
  30. export type ColumnValueType = ColumnType | 'never'; // Matches to nothing
  31. export type ParsedFunction = {
  32. arguments: string[];
  33. name: string;
  34. };
  35. type ValidateColumnValueFunction = ({name: string, dataType: ColumnType}) => boolean;
  36. export type ValidateColumnTypes =
  37. | ColumnType[]
  38. | MetricsType[]
  39. | ValidateColumnValueFunction;
  40. export type AggregateParameter =
  41. | {
  42. columnTypes: Readonly<ValidateColumnTypes>;
  43. kind: 'column';
  44. required: boolean;
  45. defaultValue?: string;
  46. }
  47. | {
  48. dataType: ColumnType;
  49. kind: 'value';
  50. required: boolean;
  51. defaultValue?: string;
  52. placeholder?: string;
  53. }
  54. | {
  55. dataType: string;
  56. kind: 'dropdown';
  57. options: SelectValue<string>[];
  58. required: boolean;
  59. defaultValue?: string;
  60. placeholder?: string;
  61. };
  62. export type AggregationRefinement = string | undefined;
  63. // The parsed result of a Field.
  64. // Functions and Fields are handled as subtypes to enable other
  65. // code to work more simply.
  66. // This type can be converted into a Field.field using generateFieldAsString()
  67. // When an alias is defined for a field, it will be shown as a column name in the table visualization.
  68. export type QueryFieldValue =
  69. | {
  70. field: string;
  71. kind: 'field';
  72. alias?: string;
  73. }
  74. | {
  75. field: string;
  76. kind: 'calculatedField';
  77. alias?: string;
  78. }
  79. | {
  80. field: string;
  81. kind: 'equation';
  82. alias?: string;
  83. }
  84. | {
  85. function: [AggregationKey, string, AggregationRefinement, AggregationRefinement];
  86. kind: 'function';
  87. alias?: string;
  88. };
  89. // Column is just an alias of a Query value
  90. export type Column = QueryFieldValue;
  91. export type Alignments = 'left' | 'right';
  92. const CONDITIONS_ARGUMENTS: SelectValue<string>[] = [
  93. {
  94. label: 'is equal to',
  95. value: 'equals',
  96. },
  97. {
  98. label: 'is not equal to',
  99. value: 'notEquals',
  100. },
  101. {
  102. label: 'is less than',
  103. value: 'less',
  104. },
  105. {
  106. label: 'is greater than',
  107. value: 'greater',
  108. },
  109. {
  110. label: 'is less than or equal to',
  111. value: 'lessOrEquals',
  112. },
  113. {
  114. label: 'is greater than or equal to',
  115. value: 'greaterOrEquals',
  116. },
  117. ];
  118. const WEB_VITALS_QUALITY: SelectValue<string>[] = [
  119. {
  120. label: 'good',
  121. value: 'good',
  122. },
  123. {
  124. label: 'meh',
  125. value: 'meh',
  126. },
  127. {
  128. label: 'poor',
  129. value: 'poor',
  130. },
  131. {
  132. label: 'any',
  133. value: 'any',
  134. },
  135. ];
  136. export enum WebVital {
  137. FP = 'measurements.fp',
  138. FCP = 'measurements.fcp',
  139. LCP = 'measurements.lcp',
  140. FID = 'measurements.fid',
  141. CLS = 'measurements.cls',
  142. TTFB = 'measurements.ttfb',
  143. RequestTime = 'measurements.ttfb.requesttime',
  144. }
  145. export enum MobileVital {
  146. AppStartCold = 'measurements.app_start_cold',
  147. AppStartWarm = 'measurements.app_start_warm',
  148. FramesTotal = 'measurements.frames_total',
  149. FramesSlow = 'measurements.frames_slow',
  150. FramesFrozen = 'measurements.frames_frozen',
  151. FramesSlowRate = 'measurements.frames_slow_rate',
  152. FramesFrozenRate = 'measurements.frames_frozen_rate',
  153. StallCount = 'measurements.stall_count',
  154. StallTotalTime = 'measurements.stall_total_time',
  155. StallLongestTime = 'measurements.stall_longest_time',
  156. StallPercentage = 'measurements.stall_percentage',
  157. }
  158. // Refer to src/sentry/search/events/fields.py
  159. // Try to keep functions logically sorted, ie. all the count functions are grouped together
  160. export const AGGREGATIONS = {
  161. count: {
  162. parameters: [],
  163. documentation: t('number of events'),
  164. outputType: 'number',
  165. isSortable: true,
  166. multiPlotType: 'area',
  167. },
  168. count_unique: {
  169. parameters: [
  170. {
  171. kind: 'column',
  172. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  173. defaultValue: 'user',
  174. required: true,
  175. },
  176. ],
  177. documentation: t('unique number of events'),
  178. outputType: 'integer',
  179. isSortable: true,
  180. multiPlotType: 'line',
  181. },
  182. count_miserable: {
  183. getFieldOverrides({parameter}: DefaultValueInputs) {
  184. if (parameter.kind === 'column') {
  185. return {defaultValue: 'user'};
  186. }
  187. return {
  188. defaultValue: parameter.defaultValue,
  189. };
  190. },
  191. parameters: [
  192. {
  193. kind: 'column',
  194. columnTypes: validateAllowedColumns(['user']),
  195. defaultValue: 'user',
  196. required: true,
  197. },
  198. {
  199. kind: 'value',
  200. dataType: 'number',
  201. defaultValue: '300',
  202. required: true,
  203. },
  204. ],
  205. documentation: t('miserable number of events'),
  206. outputType: 'number',
  207. isSortable: true,
  208. multiPlotType: 'area',
  209. },
  210. count_if: {
  211. parameters: [
  212. {
  213. kind: 'column',
  214. columnTypes: validateDenyListColumns(
  215. ['string', 'duration', 'number'],
  216. ['id', 'issue', 'user.display']
  217. ),
  218. defaultValue: 'transaction.duration',
  219. required: true,
  220. },
  221. {
  222. kind: 'dropdown',
  223. options: CONDITIONS_ARGUMENTS,
  224. dataType: 'string',
  225. defaultValue: CONDITIONS_ARGUMENTS[0].value,
  226. required: true,
  227. },
  228. {
  229. kind: 'value',
  230. dataType: 'string',
  231. defaultValue: '300',
  232. required: true,
  233. },
  234. ],
  235. documentation: t('conditional number of events'),
  236. outputType: 'number',
  237. isSortable: true,
  238. multiPlotType: 'area',
  239. },
  240. count_web_vitals: {
  241. parameters: [
  242. {
  243. kind: 'column',
  244. columnTypes: validateAllowedColumns([
  245. WebVital.LCP,
  246. WebVital.FP,
  247. WebVital.FCP,
  248. WebVital.FID,
  249. WebVital.CLS,
  250. ]),
  251. defaultValue: WebVital.LCP,
  252. required: true,
  253. },
  254. {
  255. kind: 'dropdown',
  256. options: WEB_VITALS_QUALITY,
  257. dataType: 'string',
  258. defaultValue: WEB_VITALS_QUALITY[0].value,
  259. required: true,
  260. },
  261. ],
  262. documentation: t('events matching vital thresholds'),
  263. outputType: 'number',
  264. isSortable: true,
  265. multiPlotType: 'area',
  266. },
  267. eps: {
  268. parameters: [],
  269. documentation: t('events per second'),
  270. outputType: 'number',
  271. isSortable: true,
  272. multiPlotType: 'area',
  273. },
  274. epm: {
  275. parameters: [],
  276. documentation: t('events per minute'),
  277. outputType: 'number',
  278. isSortable: true,
  279. multiPlotType: 'area',
  280. },
  281. failure_count: {
  282. parameters: [],
  283. documentation: t('number of failed events'),
  284. outputType: 'number',
  285. isSortable: true,
  286. multiPlotType: 'line',
  287. },
  288. min: {
  289. parameters: [
  290. {
  291. kind: 'column',
  292. columnTypes: validateForNumericAggregate([
  293. 'integer',
  294. 'number',
  295. 'duration',
  296. 'date',
  297. 'percentage',
  298. ]),
  299. defaultValue: 'transaction.duration',
  300. required: true,
  301. },
  302. ],
  303. documentation: t('minimum'),
  304. outputType: null,
  305. isSortable: true,
  306. multiPlotType: 'line',
  307. },
  308. max: {
  309. parameters: [
  310. {
  311. kind: 'column',
  312. columnTypes: validateForNumericAggregate([
  313. 'integer',
  314. 'number',
  315. 'duration',
  316. 'date',
  317. 'percentage',
  318. ]),
  319. defaultValue: 'transaction.duration',
  320. required: true,
  321. },
  322. ],
  323. documentation: t('maximum'),
  324. outputType: null,
  325. isSortable: true,
  326. multiPlotType: 'line',
  327. },
  328. sum: {
  329. parameters: [
  330. {
  331. kind: 'column',
  332. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  333. required: true,
  334. defaultValue: 'transaction.duration',
  335. },
  336. ],
  337. documentation: t('total value'),
  338. outputType: null,
  339. isSortable: true,
  340. multiPlotType: 'area',
  341. },
  342. any: {
  343. parameters: [
  344. {
  345. kind: 'column',
  346. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  347. required: true,
  348. defaultValue: 'transaction.duration',
  349. },
  350. ],
  351. documentation: t('pick any value'),
  352. outputType: null,
  353. isSortable: true,
  354. },
  355. p50: {
  356. parameters: [
  357. {
  358. kind: 'column',
  359. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  360. defaultValue: 'transaction.duration',
  361. required: false,
  362. },
  363. ],
  364. documentation: t('median'),
  365. outputType: null,
  366. isSortable: true,
  367. multiPlotType: 'line',
  368. },
  369. p75: {
  370. parameters: [
  371. {
  372. kind: 'column',
  373. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  374. defaultValue: 'transaction.duration',
  375. required: false,
  376. },
  377. ],
  378. documentation: t('75th percentile'),
  379. outputType: null,
  380. isSortable: true,
  381. multiPlotType: 'line',
  382. },
  383. p95: {
  384. parameters: [
  385. {
  386. kind: 'column',
  387. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  388. defaultValue: 'transaction.duration',
  389. required: false,
  390. },
  391. ],
  392. documentation: t('95th percentile'),
  393. outputType: null,
  394. type: [],
  395. isSortable: true,
  396. multiPlotType: 'line',
  397. },
  398. p99: {
  399. parameters: [
  400. {
  401. kind: 'column',
  402. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  403. defaultValue: 'transaction.duration',
  404. required: false,
  405. },
  406. ],
  407. documentation: t('99th percentile'),
  408. outputType: null,
  409. isSortable: true,
  410. multiPlotType: 'line',
  411. },
  412. p100: {
  413. parameters: [
  414. {
  415. kind: 'column',
  416. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  417. defaultValue: 'transaction.duration',
  418. required: false,
  419. },
  420. ],
  421. documentation: t('maximum'),
  422. outputType: null,
  423. isSortable: true,
  424. multiPlotType: 'line',
  425. },
  426. percentile: {
  427. parameters: [
  428. {
  429. kind: 'column',
  430. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  431. defaultValue: 'transaction.duration',
  432. required: true,
  433. },
  434. {
  435. kind: 'value',
  436. dataType: 'number',
  437. defaultValue: '0.5',
  438. required: true,
  439. },
  440. ],
  441. documentation: t('arbitrary percentile'),
  442. outputType: null,
  443. isSortable: true,
  444. multiPlotType: 'line',
  445. },
  446. avg: {
  447. parameters: [
  448. {
  449. kind: 'column',
  450. columnTypes: validateForNumericAggregate(['duration', 'number', 'percentage']),
  451. defaultValue: 'transaction.duration',
  452. required: true,
  453. },
  454. ],
  455. documentation: t('average'),
  456. outputType: null,
  457. isSortable: true,
  458. multiPlotType: 'line',
  459. },
  460. apdex: {
  461. parameters: [
  462. {
  463. kind: 'value',
  464. dataType: 'number',
  465. defaultValue: '300',
  466. required: true,
  467. },
  468. ],
  469. documentation: t('performance score, higher is better'),
  470. outputType: 'number',
  471. isSortable: true,
  472. multiPlotType: 'line',
  473. },
  474. user_misery: {
  475. parameters: [
  476. {
  477. kind: 'value',
  478. dataType: 'number',
  479. defaultValue: '300',
  480. required: true,
  481. },
  482. ],
  483. documentation: t('ratio of miserable users'),
  484. outputType: 'number',
  485. isSortable: true,
  486. multiPlotType: 'line',
  487. },
  488. failure_rate: {
  489. parameters: [],
  490. documentation: t('failure_count()/count()'),
  491. outputType: 'percentage',
  492. isSortable: true,
  493. multiPlotType: 'line',
  494. },
  495. last_seen: {
  496. parameters: [],
  497. documentation: t('largest timestamp'),
  498. outputType: 'date',
  499. isSortable: true,
  500. },
  501. } as const;
  502. // TPM and TPS are aliases that are only used in Performance
  503. export const ALIASES = {
  504. tpm: 'epm',
  505. tps: 'eps',
  506. };
  507. assert(AGGREGATIONS as Readonly<{[key in keyof typeof AGGREGATIONS]: Aggregation}>);
  508. export type AggregationKey = keyof typeof AGGREGATIONS | keyof typeof ALIASES | '';
  509. export type AggregationOutputType = Extract<
  510. ColumnType,
  511. 'number' | 'integer' | 'date' | 'duration' | 'percentage' | 'string'
  512. >;
  513. export type PlotType = 'bar' | 'line' | 'area';
  514. type DefaultValueInputs = {
  515. parameter: AggregateParameter;
  516. };
  517. export type Aggregation = {
  518. /**
  519. * Can this function be used in a sort result
  520. */
  521. isSortable: boolean;
  522. /**
  523. * The output type. Null means to inherit from the field.
  524. */
  525. outputType: AggregationOutputType | null;
  526. /**
  527. * List of parameters for the function.
  528. */
  529. parameters: Readonly<AggregateParameter[]>;
  530. getFieldOverrides?: (
  531. data: DefaultValueInputs
  532. ) => Partial<Omit<AggregateParameter, 'kind'>>;
  533. /**
  534. * How this function should be plotted when shown in a multiseries result (top5)
  535. * Optional because some functions cannot be plotted (strings/dates)
  536. */
  537. multiPlotType?: PlotType;
  538. };
  539. enum FieldKey {
  540. CULPRIT = 'culprit',
  541. DEVICE_ARCH = 'device.arch',
  542. DEVICE_BATTERY_LEVEL = 'device.battery_level',
  543. DEVICE_BRAND = 'device.brand',
  544. DEVICE_CHARGING = 'device.charging',
  545. DEVICE_FAMILY = 'device.family',
  546. DEVICE_LOCALE = 'device.locale',
  547. DEVICE_NAME = 'device.name',
  548. DEVICE_ONLINE = 'device.online',
  549. DEVICE_ORIENTATION = 'device.orientation',
  550. DEVICE_SIMULATOR = 'device.simulator',
  551. DEVICE_UUID = 'device.uuid',
  552. DIST = 'dist',
  553. ENVIRONMENT = 'environment',
  554. ERROR_HANDLED = 'error.handled',
  555. ERROR_UNHANDLED = 'error.unhandled',
  556. ERROR_MECHANISM = 'error.mechanism',
  557. ERROR_TYPE = 'error.type',
  558. ERROR_VALUE = 'error.value',
  559. EVENT_TYPE = 'event.type',
  560. GEO_CITY = 'geo.city',
  561. GEO_COUNTRY_CODE = 'geo.country_code',
  562. GEO_REGION = 'geo.region',
  563. HTTP_METHOD = 'http.method',
  564. HTTP_REFERER = 'http.referer',
  565. HTTP_URL = 'http.url',
  566. ID = 'id',
  567. ISSUE = 'issue',
  568. LEVEL = 'level',
  569. LOCATION = 'location',
  570. MESSAGE = 'message',
  571. OS_BUILD = 'os.build',
  572. OS_KERNEL_VERSION = 'os.kernel_version',
  573. PLATFORM_NAME = 'platform.name',
  574. PROJECT = 'project',
  575. RELEASE = 'release',
  576. SDK_NAME = 'sdk.name',
  577. SDK_VERSION = 'sdk.version',
  578. STACK_ABS_PATH = 'stack.abs_path',
  579. STACK_COLNO = 'stack.colno',
  580. STACK_FILENAME = 'stack.filename',
  581. STACK_FUNCTION = 'stack.function',
  582. STACK_IN_APP = 'stack.in_app',
  583. STACK_LINENO = 'stack.lineno',
  584. STACK_MODULE = 'stack.module',
  585. STACK_PACKAGE = 'stack.package',
  586. STACK_STACK_LEVEL = 'stack.stack_level',
  587. TIMESTAMP = 'timestamp',
  588. TIMESTAMP_TO_HOUR = 'timestamp.to_hour',
  589. TIMESTAMP_TO_DAY = 'timestamp.to_day',
  590. TITLE = 'title',
  591. TRACE = 'trace',
  592. TRACE_PARENT_SPAN = 'trace.parent_span',
  593. TRACE_SPAN = 'trace.span',
  594. TRANSACTION = 'transaction',
  595. TRANSACTION_DURATION = 'transaction.duration',
  596. TRANSACTION_OP = 'transaction.op',
  597. TRANSACTION_STATUS = 'transaction.status',
  598. USER = 'user',
  599. USER_EMAIL = 'user.email',
  600. USER_ID = 'user.id',
  601. USER_IP = 'user.ip',
  602. USER_USERNAME = 'user.username',
  603. USER_DISPLAY = 'user.display',
  604. }
  605. /**
  606. * Refer to src/sentry/snuba/events.py, search for Columns
  607. */
  608. export const FIELDS: Readonly<Record<FieldKey, ColumnType>> = {
  609. [FieldKey.ID]: 'string',
  610. // issue.id and project.id are omitted on purpose.
  611. // Customers should use `issue` and `project` instead.
  612. [FieldKey.TIMESTAMP]: 'date',
  613. // time is omitted on purpose.
  614. // Customers should use `timestamp` or `timestamp.to_hour`.
  615. [FieldKey.TIMESTAMP_TO_HOUR]: 'date',
  616. [FieldKey.TIMESTAMP_TO_DAY]: 'date',
  617. [FieldKey.CULPRIT]: 'string',
  618. [FieldKey.LOCATION]: 'string',
  619. [FieldKey.MESSAGE]: 'string',
  620. [FieldKey.PLATFORM_NAME]: 'string',
  621. [FieldKey.ENVIRONMENT]: 'string',
  622. [FieldKey.RELEASE]: 'string',
  623. [FieldKey.DIST]: 'string',
  624. [FieldKey.TITLE]: 'string',
  625. [FieldKey.EVENT_TYPE]: 'string',
  626. // tags.key and tags.value are omitted on purpose as well.
  627. [FieldKey.TRANSACTION]: 'string',
  628. [FieldKey.USER]: 'string',
  629. [FieldKey.USER_ID]: 'string',
  630. [FieldKey.USER_EMAIL]: 'string',
  631. [FieldKey.USER_USERNAME]: 'string',
  632. [FieldKey.USER_IP]: 'string',
  633. [FieldKey.SDK_NAME]: 'string',
  634. [FieldKey.SDK_VERSION]: 'string',
  635. [FieldKey.HTTP_METHOD]: 'string',
  636. [FieldKey.HTTP_REFERER]: 'string',
  637. [FieldKey.HTTP_URL]: 'string',
  638. [FieldKey.OS_BUILD]: 'string',
  639. [FieldKey.OS_KERNEL_VERSION]: 'string',
  640. [FieldKey.DEVICE_NAME]: 'string',
  641. [FieldKey.DEVICE_BRAND]: 'string',
  642. [FieldKey.DEVICE_LOCALE]: 'string',
  643. [FieldKey.DEVICE_UUID]: 'string',
  644. [FieldKey.DEVICE_ARCH]: 'string',
  645. [FieldKey.DEVICE_FAMILY]: 'string',
  646. [FieldKey.DEVICE_BATTERY_LEVEL]: 'number',
  647. [FieldKey.DEVICE_ORIENTATION]: 'string',
  648. [FieldKey.DEVICE_SIMULATOR]: 'boolean',
  649. [FieldKey.DEVICE_ONLINE]: 'boolean',
  650. [FieldKey.DEVICE_CHARGING]: 'boolean',
  651. [FieldKey.GEO_COUNTRY_CODE]: 'string',
  652. [FieldKey.GEO_REGION]: 'string',
  653. [FieldKey.GEO_CITY]: 'string',
  654. [FieldKey.ERROR_TYPE]: 'string',
  655. [FieldKey.ERROR_VALUE]: 'string',
  656. [FieldKey.ERROR_MECHANISM]: 'string',
  657. [FieldKey.ERROR_HANDLED]: 'boolean',
  658. [FieldKey.ERROR_UNHANDLED]: 'boolean',
  659. [FieldKey.LEVEL]: 'string',
  660. [FieldKey.STACK_ABS_PATH]: 'string',
  661. [FieldKey.STACK_FILENAME]: 'string',
  662. [FieldKey.STACK_PACKAGE]: 'string',
  663. [FieldKey.STACK_MODULE]: 'string',
  664. [FieldKey.STACK_FUNCTION]: 'string',
  665. [FieldKey.STACK_IN_APP]: 'boolean',
  666. [FieldKey.STACK_COLNO]: 'number',
  667. [FieldKey.STACK_LINENO]: 'number',
  668. [FieldKey.STACK_STACK_LEVEL]: 'number',
  669. // contexts.key and contexts.value omitted on purpose.
  670. // Transaction event fields.
  671. [FieldKey.TRANSACTION_DURATION]: 'duration',
  672. [FieldKey.TRANSACTION_OP]: 'string',
  673. [FieldKey.TRANSACTION_STATUS]: 'string',
  674. [FieldKey.TRACE]: 'string',
  675. [FieldKey.TRACE_SPAN]: 'string',
  676. [FieldKey.TRACE_PARENT_SPAN]: 'string',
  677. // Field alises defined in src/sentry/api/event_search.py
  678. [FieldKey.PROJECT]: 'string',
  679. [FieldKey.ISSUE]: 'string',
  680. [FieldKey.USER_DISPLAY]: 'string',
  681. };
  682. export const DEPRECATED_FIELDS: string[] = [FieldKey.CULPRIT];
  683. export type FieldTag = {
  684. key: FieldKey;
  685. name: FieldKey;
  686. };
  687. export const FIELD_TAGS = Object.freeze(
  688. Object.fromEntries(Object.keys(FIELDS).map(item => [item, {key: item, name: item}]))
  689. );
  690. export const SEMVER_TAGS = {
  691. 'release.version': {
  692. key: 'release.version',
  693. name: 'release.version',
  694. },
  695. 'release.build': {
  696. key: 'release.build',
  697. name: 'release.build',
  698. },
  699. 'release.package': {
  700. key: 'release.package',
  701. name: 'release.package',
  702. },
  703. 'release.stage': {
  704. key: 'release.stage',
  705. name: 'release.stage',
  706. predefined: true,
  707. values: RELEASE_ADOPTION_STAGES,
  708. },
  709. };
  710. /**
  711. * Some tag keys should never be formatted as `tag[...]`
  712. * when used as a filter because they are predefined.
  713. */
  714. const EXCLUDED_TAG_KEYS = new Set(['release']);
  715. export function formatTagKey(key: string): string {
  716. // Some tags may be normalized from context, but not all of them are.
  717. // This supports a user making a custom tag with the same name as one
  718. // that comes from context as all of these are also tags.
  719. if (key in FIELD_TAGS && !EXCLUDED_TAG_KEYS.has(key)) {
  720. return `tags[${key}]`;
  721. }
  722. return key;
  723. }
  724. // Allows for a less strict field key definition in cases we are returning custom strings as fields
  725. export type LooseFieldKey = FieldKey | string | '';
  726. export type MeasurementType = 'duration' | 'number' | 'integer' | 'percentage';
  727. const MEASUREMENTS: Readonly<Record<WebVital | MobileVital, MeasurementType>> = {
  728. [WebVital.FP]: 'duration',
  729. [WebVital.FCP]: 'duration',
  730. [WebVital.LCP]: 'duration',
  731. [WebVital.FID]: 'duration',
  732. [WebVital.CLS]: 'number',
  733. [WebVital.TTFB]: 'duration',
  734. [WebVital.RequestTime]: 'duration',
  735. [MobileVital.AppStartCold]: 'duration',
  736. [MobileVital.AppStartWarm]: 'duration',
  737. [MobileVital.FramesTotal]: 'integer',
  738. [MobileVital.FramesSlow]: 'integer',
  739. [MobileVital.FramesFrozen]: 'integer',
  740. [MobileVital.FramesSlowRate]: 'percentage',
  741. [MobileVital.FramesFrozenRate]: 'percentage',
  742. [MobileVital.StallCount]: 'integer',
  743. [MobileVital.StallTotalTime]: 'duration',
  744. [MobileVital.StallLongestTime]: 'duration',
  745. [MobileVital.StallPercentage]: 'percentage',
  746. };
  747. export function isSpanOperationBreakdownField(field: string) {
  748. return field.startsWith('spans.');
  749. }
  750. export const SPAN_OP_RELATIVE_BREAKDOWN_FIELD = 'span_ops_breakdown.relative';
  751. export function isRelativeSpanOperationBreakdownField(field: string) {
  752. return field === SPAN_OP_RELATIVE_BREAKDOWN_FIELD;
  753. }
  754. export const SPAN_OP_BREAKDOWN_FIELDS = [
  755. 'spans.http',
  756. 'spans.db',
  757. 'spans.browser',
  758. 'spans.resource',
  759. 'spans.ui',
  760. ];
  761. // This list contains fields/functions that are available with performance-view feature.
  762. export const TRACING_FIELDS = [
  763. 'avg',
  764. 'sum',
  765. 'transaction.duration',
  766. 'transaction.op',
  767. 'transaction.status',
  768. 'p50',
  769. 'p75',
  770. 'p95',
  771. 'p99',
  772. 'p100',
  773. 'percentile',
  774. 'failure_rate',
  775. 'apdex',
  776. 'count_miserable',
  777. 'user_misery',
  778. 'eps',
  779. 'epm',
  780. 'team_key_transaction',
  781. ...Object.keys(MEASUREMENTS),
  782. ...SPAN_OP_BREAKDOWN_FIELDS,
  783. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  784. ];
  785. export const MEASUREMENT_PATTERN = /^measurements\.([a-zA-Z0-9-_.]+)$/;
  786. export const SPAN_OP_BREAKDOWN_PATTERN = /^spans\.([a-zA-Z0-9-_.]+)$/;
  787. export function isMeasurement(field: string): boolean {
  788. const results = field.match(MEASUREMENT_PATTERN);
  789. return !!results;
  790. }
  791. export function measurementType(field: string): MeasurementType {
  792. if (MEASUREMENTS.hasOwnProperty(field)) {
  793. return MEASUREMENTS[field];
  794. }
  795. return 'number';
  796. }
  797. export function getMeasurementSlug(field: string): string | null {
  798. const results = field.match(MEASUREMENT_PATTERN);
  799. if (results && results.length >= 2) {
  800. return results[1];
  801. }
  802. return null;
  803. }
  804. const AGGREGATE_PATTERN = /^(\w+)\((.*)?\)$/;
  805. // Identical to AGGREGATE_PATTERN, but without the $ for newline, or ^ for start of line
  806. const AGGREGATE_BASE = /(\w+)\((.*)?\)/g;
  807. export function getAggregateArg(field: string): string | null {
  808. // only returns the first argument if field is an aggregate
  809. const result = parseFunction(field);
  810. if (result && result.arguments.length > 0) {
  811. return result.arguments[0];
  812. }
  813. return null;
  814. }
  815. export function parseFunction(field: string): ParsedFunction | null {
  816. const results = field.match(AGGREGATE_PATTERN);
  817. if (results && results.length === 3) {
  818. return {
  819. name: results[1],
  820. arguments: parseArguments(results[1], results[2]),
  821. };
  822. }
  823. return null;
  824. }
  825. export function parseArguments(functionText: string, columnText: string): string[] {
  826. // Some functions take a quoted string for their arguments that may contain commas
  827. // This function attempts to be identical with the similarly named parse_arguments
  828. // found in src/sentry/search/events/fields.py
  829. if (
  830. (functionText !== 'to_other' &&
  831. functionText !== 'count_if' &&
  832. functionText !== 'spans_histogram') ||
  833. columnText.length === 0
  834. ) {
  835. return columnText ? columnText.split(',').map(result => result.trim()) : [];
  836. }
  837. const args: string[] = [];
  838. let quoted = false;
  839. let escaped = false;
  840. let i: number = 0;
  841. let j: number = 0;
  842. while (j < columnText.length) {
  843. if (i === j && columnText[j] === '"') {
  844. // when we see a quote at the beginning of
  845. // an argument, then this is a quoted string
  846. quoted = true;
  847. } else if (i === j && columnText[j] === ' ') {
  848. // argument has leading spaces, skip over them
  849. i += 1;
  850. } else if (quoted && !escaped && columnText[j] === '\\') {
  851. // when we see a slash inside a quoted string,
  852. // the next character is an escape character
  853. escaped = true;
  854. } else if (quoted && !escaped && columnText[j] === '"') {
  855. // when we see a non-escaped quote while inside
  856. // of a quoted string, we should end it
  857. quoted = false;
  858. } else if (quoted && escaped) {
  859. // when we are inside a quoted string and have
  860. // begun an escape character, we should end it
  861. escaped = false;
  862. } else if (quoted && columnText[j] === ',') {
  863. // when we are inside a quoted string and see
  864. // a comma, it should not be considered an
  865. // argument separator
  866. } else if (columnText[j] === ',') {
  867. // when we see a comma outside of a quoted string
  868. // it is an argument separator
  869. args.push(columnText.substring(i, j).trim());
  870. i = j + 1;
  871. }
  872. j += 1;
  873. }
  874. if (i !== j) {
  875. // add in the last argument if any
  876. args.push(columnText.substring(i).trim());
  877. }
  878. return args;
  879. }
  880. // `|` is an invalid field character, so it is used to determine whether a field is an equation or not
  881. export const EQUATION_PREFIX = 'equation|';
  882. const EQUATION_ALIAS_PATTERN = /^equation\[(\d+)\]$/;
  883. export const CALCULATED_FIELD_PREFIX = 'calculated|';
  884. export function isEquation(field: string): boolean {
  885. return field.startsWith(EQUATION_PREFIX);
  886. }
  887. export function isEquationAlias(field: string): boolean {
  888. return EQUATION_ALIAS_PATTERN.test(field);
  889. }
  890. export function maybeEquationAlias(field: string): boolean {
  891. return field.includes(EQUATION_PREFIX);
  892. }
  893. export function stripEquationPrefix(field: string): string {
  894. return field.replace(EQUATION_PREFIX, '');
  895. }
  896. export function getEquationAliasIndex(field: string): number {
  897. const results = field.match(EQUATION_ALIAS_PATTERN);
  898. if (results && results.length === 2) {
  899. return parseInt(results[1], 10);
  900. }
  901. return -1;
  902. }
  903. export function getEquation(field: string): string {
  904. return field.slice(EQUATION_PREFIX.length);
  905. }
  906. export function isAggregateEquation(field: string): boolean {
  907. const results = field.match(AGGREGATE_BASE);
  908. return isEquation(field) && results !== null && results.length > 0;
  909. }
  910. export function isLegalEquationColumn(column: Column): boolean {
  911. // Any isn't allowed in arithmetic
  912. if (column.kind === 'function' && column.function[0] === 'any') {
  913. return false;
  914. }
  915. const columnType = getColumnType(column);
  916. return columnType === 'number' || columnType === 'integer' || columnType === 'duration';
  917. }
  918. export function generateAggregateFields(
  919. organization: Organization,
  920. eventFields: readonly Field[] | Field[],
  921. excludeFields: readonly string[] = []
  922. ): Field[] {
  923. const functions = Object.keys(AGGREGATIONS);
  924. const fields = Object.values(eventFields).map(field => field.field);
  925. functions.forEach(func => {
  926. const parameters = AGGREGATIONS[func].parameters.map(param => {
  927. const overrides = AGGREGATIONS[func].getFieldOverrides;
  928. if (typeof overrides === 'undefined') {
  929. return param;
  930. }
  931. return {
  932. ...param,
  933. ...overrides({parameter: param, organization}),
  934. };
  935. });
  936. if (parameters.every(param => typeof param.defaultValue !== 'undefined')) {
  937. const newField = `${func}(${parameters
  938. .map(param => param.defaultValue)
  939. .join(',')})`;
  940. if (fields.indexOf(newField) === -1 && excludeFields.indexOf(newField) === -1) {
  941. fields.push(newField);
  942. }
  943. }
  944. });
  945. return fields.map(field => ({field})) as Field[];
  946. }
  947. export function isDerivedMetric(field: string): boolean {
  948. return field.startsWith(CALCULATED_FIELD_PREFIX);
  949. }
  950. export function stripDerivedMetricsPrefix(field: string): string {
  951. return field.replace(CALCULATED_FIELD_PREFIX, '');
  952. }
  953. export function explodeFieldString(field: string, alias?: string): Column {
  954. if (isEquation(field)) {
  955. return {kind: 'equation', field: getEquation(field), alias};
  956. }
  957. if (isDerivedMetric(field)) {
  958. return {kind: 'calculatedField', field: stripDerivedMetricsPrefix(field), alias};
  959. }
  960. const results = parseFunction(field);
  961. if (results) {
  962. return {
  963. kind: 'function',
  964. function: [
  965. results.name as AggregationKey,
  966. results.arguments[0] ?? '',
  967. results.arguments[1] as AggregationRefinement,
  968. results.arguments[2] as AggregationRefinement,
  969. ],
  970. alias,
  971. };
  972. }
  973. return {kind: 'field', field, alias};
  974. }
  975. export function generateFieldAsString(value: QueryFieldValue): string {
  976. if (value.kind === 'field') {
  977. return value.field;
  978. }
  979. if (value.kind === 'calculatedField') {
  980. return `${CALCULATED_FIELD_PREFIX}${value.field}`;
  981. }
  982. if (value.kind === 'equation') {
  983. return `${EQUATION_PREFIX}${value.field}`;
  984. }
  985. const aggregation = value.function[0];
  986. const parameters = value.function.slice(1).filter(i => i);
  987. return `${aggregation}(${parameters.join(',')})`;
  988. }
  989. export function explodeField(field: Field): Column {
  990. return explodeFieldString(field.field, field.alias);
  991. }
  992. /**
  993. * Get the alias that the API results will have for a given aggregate function name
  994. */
  995. export function getAggregateAlias(field: string): string {
  996. const result = parseFunction(field);
  997. if (!result) {
  998. return field;
  999. }
  1000. let alias = result.name;
  1001. if (result.arguments.length > 0) {
  1002. alias += '_' + result.arguments.join('_');
  1003. }
  1004. return alias.replace(/[^\w]/g, '_').replace(/^_+/g, '').replace(/_+$/, '');
  1005. }
  1006. /**
  1007. * Check if a field name looks like an aggregate function or known aggregate alias.
  1008. */
  1009. export function isAggregateField(field: string): boolean {
  1010. return parseFunction(field) !== null;
  1011. }
  1012. export function isAggregateFieldOrEquation(field: string): boolean {
  1013. return isAggregateField(field) || isAggregateEquation(field) || isNumericMetrics(field);
  1014. }
  1015. /**
  1016. * Temporary hardcoded hack to enable testing derived metrics.
  1017. * Can be removed after we get rid of getAggregateFields
  1018. */
  1019. export function isNumericMetrics(field: string): boolean {
  1020. return [
  1021. 'session.crash_free_rate',
  1022. 'session.crashed',
  1023. 'session.errored_preaggregated',
  1024. 'session.errored_set',
  1025. 'session.init',
  1026. ].includes(field);
  1027. }
  1028. export const FIELDS_DOCS: Readonly<Record<string, string>> = {
  1029. has: t('check field exists'),
  1030. 'release.version': t('semantic version'),
  1031. 'release.stage': t('adoption stage'),
  1032. 'release.package': t('semantic package'),
  1033. 'release.build': t('semantic build number'),
  1034. [FieldKey.CULPRIT]: t('deprecated'),
  1035. [FieldKey.DEVICE_ARCH]: t('cpu architecture'),
  1036. [FieldKey.DEVICE_FAMILY]: t('common part of name'),
  1037. [FieldKey.DEVICE_BATTERY_LEVEL]: t('0-100 of level'),
  1038. [FieldKey.DEVICE_BRAND]: t('brand of device'),
  1039. [FieldKey.DEVICE_CHARGING]: t('True or False'),
  1040. [FieldKey.DEVICE_LOCALE]: t('deprecated'),
  1041. [FieldKey.DEVICE_NAME]: t('details of device'),
  1042. [FieldKey.DEVICE_ONLINE]: t('True or False'),
  1043. [FieldKey.DEVICE_ORIENTATION]: t('portrait or landscape'),
  1044. [FieldKey.DEVICE_SIMULATOR]: t('True or False'),
  1045. [FieldKey.DEVICE_UUID]: t('uuid'),
  1046. [FieldKey.DIST]: t('build or variant'),
  1047. [FieldKey.ENVIRONMENT]: t('deployment name'),
  1048. [FieldKey.ERROR_HANDLED]: t('True or False'),
  1049. [FieldKey.ERROR_MECHANISM]: t('created error'),
  1050. [FieldKey.ERROR_TYPE]: t('exception type'),
  1051. [FieldKey.ERROR_UNHANDLED]: t('True or False'),
  1052. [FieldKey.ERROR_VALUE]: t('error value'),
  1053. [FieldKey.EVENT_TYPE]: t('type of event'),
  1054. [FieldKey.GEO_CITY]: t('full name'),
  1055. [FieldKey.GEO_COUNTRY_CODE]: t('ISO 3166-1'),
  1056. [FieldKey.GEO_REGION]: t('full name'),
  1057. [FieldKey.HTTP_METHOD]: t('method of request'),
  1058. [FieldKey.HTTP_REFERER]: t('referer of request'),
  1059. [FieldKey.HTTP_URL]: t('url of request'),
  1060. [FieldKey.ID]: t('event identifier'),
  1061. [FieldKey.ISSUE]: t('issue short id'),
  1062. [FieldKey.LEVEL]: t('string'),
  1063. [FieldKey.LOCATION]: t('where error happened'),
  1064. [FieldKey.MESSAGE]: t('title or name'),
  1065. [FieldKey.OS_BUILD]: t('internal build revision'),
  1066. [FieldKey.OS_KERNEL_VERSION]: t('kernel string'),
  1067. [FieldKey.PLATFORM_NAME]: t('name of platform'),
  1068. [FieldKey.PROJECT]: t('project name'),
  1069. [FieldKey.RELEASE]: t('code version'),
  1070. [FieldKey.SDK_NAME]: t('sentry sdk name'),
  1071. [FieldKey.SDK_VERSION]: t('sentry sdk version'),
  1072. [FieldKey.STACK_ABS_PATH]: t('absolute path'),
  1073. [FieldKey.STACK_COLNO]: t('column number'),
  1074. [FieldKey.STACK_FILENAME]: t('source file'),
  1075. [FieldKey.STACK_FUNCTION]: t('function called'),
  1076. [FieldKey.STACK_IN_APP]: t('True or False'),
  1077. [FieldKey.STACK_LINENO]: t('line number'),
  1078. [FieldKey.STACK_MODULE]: t('platform specific'),
  1079. [FieldKey.STACK_PACKAGE]: t('package of frame'),
  1080. [FieldKey.STACK_STACK_LEVEL]: t('number'),
  1081. [FieldKey.TIMESTAMP]: t('time event occurred'),
  1082. [FieldKey.TIMESTAMP_TO_DAY]: t('rounded timestamp'),
  1083. [FieldKey.TIMESTAMP_TO_HOUR]: t('rounded timestamp'),
  1084. [FieldKey.TITLE]: t('title or name'),
  1085. [FieldKey.TRACE]: t('uuid'),
  1086. [FieldKey.TRACE_PARENT_SPAN]: t('span for parent trace'),
  1087. [FieldKey.TRACE_SPAN]: t('span for trace'),
  1088. [FieldKey.TRANSACTION]: t('transaction name'),
  1089. [FieldKey.TRANSACTION_DURATION]: t('duration'),
  1090. [FieldKey.TRANSACTION_OP]: t('short code'),
  1091. [FieldKey.TRANSACTION_STATUS]: t('final status'),
  1092. [FieldKey.USER]: t('unparsed user field'),
  1093. [FieldKey.USER_DISPLAY]: t('email>username>id>ip'),
  1094. [FieldKey.USER_EMAIL]: t('email'),
  1095. [FieldKey.USER_ID]: t('identifier'),
  1096. [FieldKey.USER_IP]: t('ip address'),
  1097. [FieldKey.USER_USERNAME]: t('username'),
  1098. [MobileVital.AppStartCold]: t('first launch duration'),
  1099. [MobileVital.AppStartWarm]: t('subsequent launch duration'),
  1100. [MobileVital.FramesFrozenRate]: t('frames_frozen/frames_total'),
  1101. [MobileVital.FramesFrozen]: t('framse slower than 700ms'),
  1102. [MobileVital.FramesSlowRate]: t('frames_slow/frames_total'),
  1103. [MobileVital.FramesSlow]: t('frames slower than 16ms'),
  1104. [MobileVital.FramesTotal]: t('number of frames'),
  1105. [MobileVital.StallCount]: t('stalled event loops'),
  1106. [MobileVital.StallLongestTime]: t('largest stalled event loop'),
  1107. [MobileVital.StallPercentage]: t('stall_total_time/duration'),
  1108. [MobileVital.StallTotalTime]: t('duration of stall'),
  1109. [WebVital.CLS]: t('cumulative layout shift'),
  1110. [WebVital.FCP]: t('first contentful paint'),
  1111. [WebVital.FID]: t('first input delay'),
  1112. [WebVital.FP]: t('first paint'),
  1113. [WebVital.LCP]: t('largest contentful paint'),
  1114. [WebVital.RequestTime]: t('time until response start'),
  1115. [WebVital.TTFB]: t('time to first byte'),
  1116. };
  1117. export function getFieldDoc(field: string): React.ReactNode {
  1118. if (FIELDS_DOCS.hasOwnProperty(field)) {
  1119. return FIELDS_DOCS[field];
  1120. }
  1121. const parsed = parseFunction(field);
  1122. if (parsed && AGGREGATIONS.hasOwnProperty(parsed.name)) {
  1123. return AGGREGATIONS[parsed.name].documentation;
  1124. }
  1125. return '';
  1126. }
  1127. export function getAggregateFields(fields: string[]): string[] {
  1128. return fields.filter(
  1129. field =>
  1130. isAggregateField(field) || isAggregateEquation(field) || isNumericMetrics(field)
  1131. );
  1132. }
  1133. export function getColumnsAndAggregates(fields: string[]): {
  1134. aggregates: string[];
  1135. columns: string[];
  1136. } {
  1137. const aggregates = getAggregateFields(fields);
  1138. const columns = fields.filter(field => !!!aggregates.includes(field));
  1139. return {columns, aggregates};
  1140. }
  1141. export function getColumnsAndAggregatesAsStrings(fields: QueryFieldValue[]): {
  1142. aggregates: string[];
  1143. columns: string[];
  1144. fieldAliases: string[];
  1145. } {
  1146. // TODO(dam): distinguish between metrics, derived metrics and tags
  1147. const aggregateFields: string[] = [];
  1148. const nonAggregateFields: string[] = [];
  1149. const fieldAliases: string[] = [];
  1150. for (const field of fields) {
  1151. const fieldString = generateFieldAsString(field);
  1152. if (field.kind === 'function' || field.kind === 'calculatedField') {
  1153. aggregateFields.push(fieldString);
  1154. } else if (field.kind === 'equation') {
  1155. if (isAggregateEquation(fieldString)) {
  1156. aggregateFields.push(fieldString);
  1157. } else {
  1158. nonAggregateFields.push(fieldString);
  1159. }
  1160. } else {
  1161. nonAggregateFields.push(fieldString);
  1162. }
  1163. fieldAliases.push(field.alias ?? '');
  1164. }
  1165. return {aggregates: aggregateFields, columns: nonAggregateFields, fieldAliases};
  1166. }
  1167. /**
  1168. * Convert a function string into type it will output.
  1169. * This is useful when you need to format values in tooltips,
  1170. * or in series markers.
  1171. */
  1172. export function aggregateOutputType(field: string): AggregationOutputType {
  1173. const result = parseFunction(field);
  1174. if (!result) {
  1175. return 'number';
  1176. }
  1177. const outputType = aggregateFunctionOutputType(result.name, result.arguments[0]);
  1178. if (outputType === null) {
  1179. return 'number';
  1180. }
  1181. return outputType;
  1182. }
  1183. /**
  1184. * Converts a function string and its first argument into its output type.
  1185. * - If the function has a fixed output type, that will be the result.
  1186. * - If the function does not define an output type, the output type will be equal to
  1187. * the type of its first argument.
  1188. * - If the function has an optional first argument, and it was not defined, make sure
  1189. * to use the default argument as the first argument.
  1190. * - If the type could not be determined, return null.
  1191. */
  1192. export function aggregateFunctionOutputType(
  1193. funcName: string,
  1194. firstArg: string | undefined
  1195. ): AggregationOutputType | null {
  1196. const aggregate =
  1197. AGGREGATIONS[ALIASES[funcName] || funcName] ?? SESSIONS_OPERATIONS[funcName];
  1198. // Attempt to use the function's outputType.
  1199. if (aggregate?.outputType) {
  1200. return aggregate.outputType;
  1201. }
  1202. // If the first argument is undefined and it is not required,
  1203. // then we attempt to get the default value.
  1204. if (!firstArg && aggregate?.parameters?.[0]) {
  1205. if (aggregate.parameters[0].required === false) {
  1206. firstArg = aggregate.parameters[0].defaultValue;
  1207. }
  1208. }
  1209. if (firstArg && SESSIONS_FIELDS.hasOwnProperty(firstArg)) {
  1210. return SESSIONS_FIELDS[firstArg].type as AggregationOutputType;
  1211. }
  1212. // If the function is an inherit type it will have a field as
  1213. // the first parameter and we can use that to get the type.
  1214. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  1215. return FIELDS[firstArg];
  1216. }
  1217. if (firstArg && isMeasurement(firstArg)) {
  1218. return measurementType(firstArg);
  1219. }
  1220. if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  1221. return 'duration';
  1222. }
  1223. return null;
  1224. }
  1225. export function errorsAndTransactionsAggregateFunctionOutputType(
  1226. funcName: string,
  1227. firstArg: string | undefined
  1228. ): AggregationOutputType | null {
  1229. const aggregate = AGGREGATIONS[ALIASES[funcName] || funcName];
  1230. // Attempt to use the function's outputType.
  1231. if (aggregate?.outputType) {
  1232. return aggregate.outputType;
  1233. }
  1234. // If the first argument is undefined and it is not required,
  1235. // then we attempt to get the default value.
  1236. if (!firstArg && aggregate?.parameters?.[0]) {
  1237. if (aggregate.parameters[0].required === false) {
  1238. firstArg = aggregate.parameters[0].defaultValue;
  1239. }
  1240. }
  1241. // If the function is an inherit type it will have a field as
  1242. // the first parameter and we can use that to get the type.
  1243. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  1244. return FIELDS[firstArg];
  1245. }
  1246. if (firstArg && isMeasurement(firstArg)) {
  1247. return measurementType(firstArg);
  1248. }
  1249. if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  1250. return 'duration';
  1251. }
  1252. return null;
  1253. }
  1254. export function sessionsAggregateFunctionOutputType(
  1255. funcName: string,
  1256. firstArg: string | undefined
  1257. ): AggregationOutputType | null {
  1258. const aggregate = SESSIONS_OPERATIONS[funcName];
  1259. // Attempt to use the function's outputType.
  1260. if (aggregate?.outputType) {
  1261. return aggregate.outputType;
  1262. }
  1263. // If the first argument is undefined and it is not required,
  1264. // then we attempt to get the default value.
  1265. if (!firstArg && aggregate?.parameters?.[0]) {
  1266. if (aggregate.parameters[0].required === false) {
  1267. firstArg = aggregate.parameters[0].defaultValue;
  1268. }
  1269. }
  1270. if (firstArg && SESSIONS_FIELDS.hasOwnProperty(firstArg)) {
  1271. return SESSIONS_FIELDS[firstArg].type as AggregationOutputType;
  1272. }
  1273. return null;
  1274. }
  1275. /**
  1276. * Get the multi-series chart type for an aggregate function.
  1277. */
  1278. export function aggregateMultiPlotType(field: string): PlotType {
  1279. if (isEquation(field)) {
  1280. return 'line';
  1281. }
  1282. const result = parseFunction(field);
  1283. // Handle invalid data.
  1284. if (!result) {
  1285. return 'area';
  1286. }
  1287. if (!AGGREGATIONS.hasOwnProperty(result.name)) {
  1288. return 'area';
  1289. }
  1290. return AGGREGATIONS[result.name].multiPlotType;
  1291. }
  1292. function validateForNumericAggregate(
  1293. validColumnTypes: ColumnType[]
  1294. ): ValidateColumnValueFunction {
  1295. return function ({name, dataType}: {dataType: ColumnType; name: string}): boolean {
  1296. // these built-in columns cannot be applied to numeric aggregates such as percentile(...)
  1297. if (
  1298. [
  1299. FieldKey.DEVICE_BATTERY_LEVEL,
  1300. FieldKey.STACK_COLNO,
  1301. FieldKey.STACK_LINENO,
  1302. FieldKey.STACK_STACK_LEVEL,
  1303. ].includes(name as FieldKey)
  1304. ) {
  1305. return false;
  1306. }
  1307. return validColumnTypes.includes(dataType);
  1308. };
  1309. }
  1310. function validateDenyListColumns(
  1311. validColumnTypes: ColumnType[],
  1312. deniedColumns: string[]
  1313. ): ValidateColumnValueFunction {
  1314. return function ({name, dataType}: {dataType: ColumnType; name: string}): boolean {
  1315. return validColumnTypes.includes(dataType) && !deniedColumns.includes(name);
  1316. };
  1317. }
  1318. function validateAllowedColumns(validColumns: string[]): ValidateColumnValueFunction {
  1319. return function ({name}): boolean {
  1320. return validColumns.includes(name);
  1321. };
  1322. }
  1323. const alignedTypes: ColumnValueType[] = ['number', 'duration', 'integer', 'percentage'];
  1324. export function fieldAlignment(
  1325. columnName: string,
  1326. columnType?: undefined | ColumnValueType,
  1327. metadata?: Record<string, ColumnValueType>
  1328. ): Alignments {
  1329. let align: Alignments = 'left';
  1330. if (columnType) {
  1331. align = alignedTypes.includes(columnType) ? 'right' : 'left';
  1332. }
  1333. if (columnType === undefined || columnType === 'never') {
  1334. // fallback to align the column based on the table metadata
  1335. const maybeType = metadata ? metadata[getAggregateAlias(columnName)] : undefined;
  1336. if (maybeType !== undefined && alignedTypes.includes(maybeType)) {
  1337. align = 'right';
  1338. }
  1339. }
  1340. return align;
  1341. }
  1342. /**
  1343. * Match on types that are legal to show on a timeseries chart.
  1344. */
  1345. export function isLegalYAxisType(match: ColumnType | MetricsType) {
  1346. return ['number', 'integer', 'duration', 'percentage'].includes(match);
  1347. }
  1348. export function getSpanOperationName(field: string): string | null {
  1349. const results = field.match(SPAN_OP_BREAKDOWN_PATTERN);
  1350. if (results && results.length >= 2) {
  1351. return results[1];
  1352. }
  1353. return null;
  1354. }
  1355. export function getColumnType(column: Column): ColumnType {
  1356. if (column.kind === 'function') {
  1357. const outputType = aggregateFunctionOutputType(
  1358. column.function[0],
  1359. column.function[1]
  1360. );
  1361. if (outputType !== null) {
  1362. return outputType;
  1363. }
  1364. } else if (column.kind === 'field') {
  1365. if (FIELDS.hasOwnProperty(column.field)) {
  1366. return FIELDS[column.field];
  1367. }
  1368. if (isMeasurement(column.field)) {
  1369. return measurementType(column.field);
  1370. }
  1371. if (isSpanOperationBreakdownField(column.field)) {
  1372. return 'duration';
  1373. }
  1374. }
  1375. return 'string';
  1376. }
  1377. export function hasDuplicate(columnList: Column[], column: Column): boolean {
  1378. if (column.kind !== 'function' && column.kind !== 'field') {
  1379. return false;
  1380. }
  1381. return columnList.filter(newColumn => isEqual(newColumn, column)).length > 1;
  1382. }