fields.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. import {LightWeightOrganization} from 'app/types';
  2. import {assert} from 'app/types/utils';
  3. export type Sort = {
  4. kind: 'asc' | 'desc';
  5. field: string;
  6. };
  7. // Contains the URL field value & the related table column width.
  8. // Can be parsed into a Column using explodeField()
  9. export type Field = {
  10. field: string;
  11. width?: number;
  12. };
  13. export type ColumnType =
  14. | 'boolean'
  15. | 'date'
  16. | 'duration'
  17. | 'integer'
  18. | 'number'
  19. | 'percentage'
  20. | 'string';
  21. export type ColumnValueType = ColumnType | 'never'; // Matches to nothing
  22. type ValidateColumnValueFunction = ({name: string, dataType: ColumnType}) => boolean;
  23. export type ValidateColumnTypes = ColumnType[] | ValidateColumnValueFunction;
  24. export type AggregateParameter =
  25. | {
  26. kind: 'column';
  27. columnTypes: Readonly<ValidateColumnTypes>;
  28. defaultValue?: string;
  29. required: boolean;
  30. }
  31. | {
  32. kind: 'value';
  33. dataType: ColumnType;
  34. defaultValue?: string;
  35. required: boolean;
  36. };
  37. export type AggregationRefinement = string | undefined;
  38. // The parsed result of a Field.
  39. // Functions and Fields are handled as subtypes to enable other
  40. // code to work more simply.
  41. // This type can be converted into a Field.field using generateFieldAsString()
  42. export type QueryFieldValue =
  43. | {
  44. kind: 'field';
  45. field: string;
  46. }
  47. | {
  48. kind: 'function';
  49. function: [AggregationKey, string, AggregationRefinement];
  50. };
  51. // Column is just an alias of a Query value
  52. export type Column = QueryFieldValue;
  53. export type Alignments = 'left' | 'right';
  54. // Refer to src/sentry/api/event_search.py
  55. export const AGGREGATIONS = {
  56. count: {
  57. parameters: [],
  58. outputType: 'number',
  59. isSortable: true,
  60. multiPlotType: 'area',
  61. },
  62. count_unique: {
  63. parameters: [
  64. {
  65. kind: 'column',
  66. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  67. required: true,
  68. },
  69. ],
  70. outputType: 'number',
  71. isSortable: true,
  72. multiPlotType: 'line',
  73. },
  74. failure_count: {
  75. parameters: [],
  76. outputType: 'number',
  77. isSortable: true,
  78. multiPlotType: 'line',
  79. },
  80. min: {
  81. parameters: [
  82. {
  83. kind: 'column',
  84. columnTypes: validateForNumericAggregate([
  85. 'integer',
  86. 'number',
  87. 'duration',
  88. 'date',
  89. ]),
  90. required: true,
  91. },
  92. ],
  93. outputType: null,
  94. isSortable: true,
  95. multiPlotType: 'line',
  96. },
  97. max: {
  98. parameters: [
  99. {
  100. kind: 'column',
  101. columnTypes: validateForNumericAggregate([
  102. 'integer',
  103. 'number',
  104. 'duration',
  105. 'date',
  106. ]),
  107. required: true,
  108. },
  109. ],
  110. outputType: null,
  111. isSortable: true,
  112. multiPlotType: 'line',
  113. },
  114. avg: {
  115. parameters: [
  116. {
  117. kind: 'column',
  118. columnTypes: validateForNumericAggregate(['duration', 'number']),
  119. defaultValue: 'transaction.duration',
  120. required: true,
  121. },
  122. ],
  123. outputType: null,
  124. isSortable: true,
  125. multiPlotType: 'line',
  126. },
  127. sum: {
  128. parameters: [
  129. {
  130. kind: 'column',
  131. columnTypes: validateForNumericAggregate(['duration', 'number']),
  132. required: true,
  133. },
  134. ],
  135. outputType: null,
  136. isSortable: true,
  137. multiPlotType: 'area',
  138. },
  139. any: {
  140. parameters: [
  141. {
  142. kind: 'column',
  143. columnTypes: ['string', 'integer', 'number', 'duration', 'date', 'boolean'],
  144. required: true,
  145. },
  146. ],
  147. outputType: null,
  148. isSortable: true,
  149. },
  150. last_seen: {
  151. parameters: [],
  152. outputType: 'date',
  153. isSortable: true,
  154. },
  155. // Tracing functions.
  156. p50: {
  157. parameters: [
  158. {
  159. kind: 'column',
  160. columnTypes: validateForNumericAggregate(['duration', 'number']),
  161. defaultValue: 'transaction.duration',
  162. required: false,
  163. },
  164. ],
  165. outputType: null,
  166. isSortable: true,
  167. multiPlotType: 'line',
  168. },
  169. p75: {
  170. parameters: [
  171. {
  172. kind: 'column',
  173. columnTypes: validateForNumericAggregate(['duration', 'number']),
  174. defaultValue: 'transaction.duration',
  175. required: false,
  176. },
  177. ],
  178. outputType: null,
  179. isSortable: true,
  180. multiPlotType: 'line',
  181. },
  182. p95: {
  183. parameters: [
  184. {
  185. kind: 'column',
  186. columnTypes: validateForNumericAggregate(['duration', 'number']),
  187. defaultValue: 'transaction.duration',
  188. required: false,
  189. },
  190. ],
  191. outputType: null,
  192. type: [],
  193. isSortable: true,
  194. multiPlotType: 'line',
  195. },
  196. p99: {
  197. parameters: [
  198. {
  199. kind: 'column',
  200. columnTypes: validateForNumericAggregate(['duration', 'number']),
  201. defaultValue: 'transaction.duration',
  202. required: false,
  203. },
  204. ],
  205. outputType: null,
  206. isSortable: true,
  207. multiPlotType: 'line',
  208. },
  209. p100: {
  210. parameters: [
  211. {
  212. kind: 'column',
  213. columnTypes: validateForNumericAggregate(['duration', 'number']),
  214. defaultValue: 'transaction.duration',
  215. required: false,
  216. },
  217. ],
  218. outputType: null,
  219. isSortable: true,
  220. multiPlotType: 'line',
  221. },
  222. percentile: {
  223. parameters: [
  224. {
  225. kind: 'column',
  226. columnTypes: validateForNumericAggregate(['duration', 'number']),
  227. defaultValue: 'transaction.duration',
  228. required: true,
  229. },
  230. {
  231. kind: 'value',
  232. dataType: 'number',
  233. defaultValue: '0.5',
  234. required: true,
  235. },
  236. ],
  237. outputType: null,
  238. isSortable: true,
  239. multiPlotType: 'line',
  240. },
  241. failure_rate: {
  242. parameters: [],
  243. outputType: 'percentage',
  244. isSortable: true,
  245. multiPlotType: 'line',
  246. },
  247. apdex: {
  248. generateDefaultValue({parameter, organization}: DefaultValueInputs) {
  249. return organization.apdexThreshold?.toString() ?? parameter.defaultValue;
  250. },
  251. parameters: [
  252. {
  253. kind: 'value',
  254. dataType: 'number',
  255. defaultValue: '300',
  256. required: true,
  257. },
  258. ],
  259. outputType: 'number',
  260. isSortable: true,
  261. multiPlotType: 'line',
  262. },
  263. user_misery: {
  264. generateDefaultValue({parameter, organization}: DefaultValueInputs) {
  265. return organization.apdexThreshold?.toString() ?? parameter.defaultValue;
  266. },
  267. parameters: [
  268. {
  269. kind: 'value',
  270. dataType: 'number',
  271. defaultValue: '300',
  272. required: true,
  273. },
  274. ],
  275. outputType: 'number',
  276. isSortable: true,
  277. multiPlotType: 'area',
  278. },
  279. eps: {
  280. parameters: [],
  281. outputType: 'number',
  282. isSortable: true,
  283. multiPlotType: 'area',
  284. },
  285. epm: {
  286. parameters: [],
  287. outputType: 'number',
  288. isSortable: true,
  289. multiPlotType: 'area',
  290. },
  291. count_miserable: {
  292. generateDefaultValue({parameter, organization}: DefaultValueInputs) {
  293. if (parameter.kind === 'column') {
  294. return 'user';
  295. }
  296. return organization.apdexThreshold?.toString() ?? parameter.defaultValue;
  297. },
  298. parameters: [
  299. {
  300. kind: 'column',
  301. columnTypes: validateAllowedColumns(['user']),
  302. defaultValue: 'user',
  303. required: true,
  304. },
  305. {
  306. kind: 'value',
  307. dataType: 'number',
  308. defaultValue: '300',
  309. required: true,
  310. },
  311. ],
  312. outputType: 'number',
  313. isSortable: true,
  314. multiPlotType: 'area',
  315. },
  316. } as const;
  317. // TPM and TPS are aliases that are only used in Performance
  318. export const ALIASES = {
  319. tpm: 'epm',
  320. tps: 'eps',
  321. };
  322. assert(AGGREGATIONS as Readonly<{[key in keyof typeof AGGREGATIONS]: Aggregation}>);
  323. export type AggregationKey = keyof typeof AGGREGATIONS | keyof typeof ALIASES | '';
  324. export type AggregationOutputType = Extract<
  325. ColumnType,
  326. 'number' | 'integer' | 'date' | 'duration' | 'percentage' | 'string'
  327. >;
  328. export type PlotType = 'bar' | 'line' | 'area';
  329. type DefaultValueInputs = {
  330. parameter: AggregateParameter;
  331. organization: LightWeightOrganization;
  332. };
  333. export type Aggregation = {
  334. /**
  335. * Used by functions that need to define their default values dynamically
  336. * based on the organization, or parameter data.
  337. */
  338. generateDefaultValue?: (data: DefaultValueInputs) => string;
  339. /**
  340. * List of parameters for the function.
  341. */
  342. parameters: Readonly<AggregateParameter[]>;
  343. /**
  344. * The output type. Null means to inherit from the field.
  345. */
  346. outputType: AggregationOutputType | null;
  347. /**
  348. * Can this function be used in a sort result
  349. */
  350. isSortable: boolean;
  351. /**
  352. * How this function should be plotted when shown in a multiseries result (top5)
  353. * Optional because some functions cannot be plotted (strings/dates)
  354. */
  355. multiPlotType?: PlotType;
  356. };
  357. enum FieldKey {
  358. CULPRIT = 'culprit',
  359. DEVICE_ARCH = 'device.arch',
  360. DEVICE_BATTERY_LEVEL = 'device.battery_level',
  361. DEVICE_BRAND = 'device.brand',
  362. DEVICE_CHARGING = 'device.charging',
  363. DEVICE_LOCALE = 'device.locale',
  364. DEVICE_NAME = 'device.name',
  365. DEVICE_ONLINE = 'device.online',
  366. DEVICE_ORIENTATION = 'device.orientation',
  367. DEVICE_SIMULATOR = 'device.simulator',
  368. DEVICE_UUID = 'device.uuid',
  369. DIST = 'dist',
  370. ENVIRONMENT = 'environment',
  371. ERROR_HANDLED = 'error.handled',
  372. ERROR_UNHANDLED = 'error.unhandled',
  373. ERROR_MECHANISM = 'error.mechanism',
  374. ERROR_TYPE = 'error.type',
  375. ERROR_VALUE = 'error.value',
  376. EVENT_TYPE = 'event.type',
  377. GEO_CITY = 'geo.city',
  378. GEO_COUNTRY_CODE = 'geo.country_code',
  379. GEO_REGION = 'geo.region',
  380. HTTP_METHOD = 'http.method',
  381. HTTP_REFERER = 'http.referer',
  382. HTTP_URL = 'http.url',
  383. ID = 'id',
  384. ISSUE = 'issue',
  385. LOCATION = 'location',
  386. MESSAGE = 'message',
  387. OS_BUILD = 'os.build',
  388. OS_KERNEL_VERSION = 'os.kernel_version',
  389. PLATFORM_NAME = 'platform.name',
  390. PROJECT = 'project',
  391. RELEASE = 'release',
  392. SDK_NAME = 'sdk.name',
  393. SDK_VERSION = 'sdk.version',
  394. STACK_ABS_PATH = 'stack.abs_path',
  395. STACK_COLNO = 'stack.colno',
  396. STACK_FILENAME = 'stack.filename',
  397. STACK_FUNCTION = 'stack.function',
  398. STACK_IN_APP = 'stack.in_app',
  399. STACK_LINENO = 'stack.lineno',
  400. STACK_MODULE = 'stack.module',
  401. STACK_PACKAGE = 'stack.package',
  402. STACK_STACK_LEVEL = 'stack.stack_level',
  403. TIMESTAMP = 'timestamp',
  404. TIMESTAMP_TO_HOUR = 'timestamp.to_hour',
  405. TIMESTAMP_TO_DAY = 'timestamp.to_day',
  406. TITLE = 'title',
  407. TRACE = 'trace',
  408. TRACE_PARENT_SPAN = 'trace.parent_span',
  409. TRACE_SPAN = 'trace.span',
  410. TRANSACTION = 'transaction',
  411. TRANSACTION_DURATION = 'transaction.duration',
  412. TRANSACTION_OP = 'transaction.op',
  413. TRANSACTION_STATUS = 'transaction.status',
  414. USER_EMAIL = 'user.email',
  415. USER_ID = 'user.id',
  416. USER_IP = 'user.ip',
  417. USER_USERNAME = 'user.username',
  418. USER_DISPLAY = 'user.display',
  419. }
  420. /**
  421. * Refer to src/sentry/snuba/events.py, search for Columns
  422. */
  423. export const FIELDS: Readonly<Record<FieldKey, ColumnType>> = {
  424. [FieldKey.ID]: 'string',
  425. // issue.id and project.id are omitted on purpose.
  426. // Customers should use `issue` and `project` instead.
  427. [FieldKey.TIMESTAMP]: 'date',
  428. // time is omitted on purpose.
  429. // Customers should use `timestamp` or `timestamp.to_hour`.
  430. [FieldKey.TIMESTAMP_TO_HOUR]: 'date',
  431. [FieldKey.TIMESTAMP_TO_DAY]: 'date',
  432. [FieldKey.CULPRIT]: 'string',
  433. [FieldKey.LOCATION]: 'string',
  434. [FieldKey.MESSAGE]: 'string',
  435. [FieldKey.PLATFORM_NAME]: 'string',
  436. [FieldKey.ENVIRONMENT]: 'string',
  437. [FieldKey.RELEASE]: 'string',
  438. [FieldKey.DIST]: 'string',
  439. [FieldKey.TITLE]: 'string',
  440. [FieldKey.EVENT_TYPE]: 'string',
  441. // tags.key and tags.value are omitted on purpose as well.
  442. [FieldKey.TRANSACTION]: 'string',
  443. [FieldKey.USER_ID]: 'string',
  444. [FieldKey.USER_EMAIL]: 'string',
  445. [FieldKey.USER_USERNAME]: 'string',
  446. [FieldKey.USER_IP]: 'string',
  447. [FieldKey.SDK_NAME]: 'string',
  448. [FieldKey.SDK_VERSION]: 'string',
  449. [FieldKey.HTTP_METHOD]: 'string',
  450. [FieldKey.HTTP_REFERER]: 'string',
  451. [FieldKey.HTTP_URL]: 'string',
  452. [FieldKey.OS_BUILD]: 'string',
  453. [FieldKey.OS_KERNEL_VERSION]: 'string',
  454. [FieldKey.DEVICE_NAME]: 'string',
  455. [FieldKey.DEVICE_BRAND]: 'string',
  456. [FieldKey.DEVICE_LOCALE]: 'string',
  457. [FieldKey.DEVICE_UUID]: 'string',
  458. [FieldKey.DEVICE_ARCH]: 'string',
  459. [FieldKey.DEVICE_BATTERY_LEVEL]: 'number',
  460. [FieldKey.DEVICE_ORIENTATION]: 'string',
  461. [FieldKey.DEVICE_SIMULATOR]: 'boolean',
  462. [FieldKey.DEVICE_ONLINE]: 'boolean',
  463. [FieldKey.DEVICE_CHARGING]: 'boolean',
  464. [FieldKey.GEO_COUNTRY_CODE]: 'string',
  465. [FieldKey.GEO_REGION]: 'string',
  466. [FieldKey.GEO_CITY]: 'string',
  467. [FieldKey.ERROR_TYPE]: 'string',
  468. [FieldKey.ERROR_VALUE]: 'string',
  469. [FieldKey.ERROR_MECHANISM]: 'string',
  470. [FieldKey.ERROR_HANDLED]: 'boolean',
  471. [FieldKey.ERROR_UNHANDLED]: 'boolean',
  472. [FieldKey.STACK_ABS_PATH]: 'string',
  473. [FieldKey.STACK_FILENAME]: 'string',
  474. [FieldKey.STACK_PACKAGE]: 'string',
  475. [FieldKey.STACK_MODULE]: 'string',
  476. [FieldKey.STACK_FUNCTION]: 'string',
  477. [FieldKey.STACK_IN_APP]: 'boolean',
  478. [FieldKey.STACK_COLNO]: 'number',
  479. [FieldKey.STACK_LINENO]: 'number',
  480. [FieldKey.STACK_STACK_LEVEL]: 'number',
  481. // contexts.key and contexts.value omitted on purpose.
  482. // Transaction event fields.
  483. [FieldKey.TRANSACTION_DURATION]: 'duration',
  484. [FieldKey.TRANSACTION_OP]: 'string',
  485. [FieldKey.TRANSACTION_STATUS]: 'string',
  486. [FieldKey.TRACE]: 'string',
  487. [FieldKey.TRACE_SPAN]: 'string',
  488. [FieldKey.TRACE_PARENT_SPAN]: 'string',
  489. // Field alises defined in src/sentry/api/event_search.py
  490. [FieldKey.PROJECT]: 'string',
  491. [FieldKey.ISSUE]: 'string',
  492. [FieldKey.USER_DISPLAY]: 'string',
  493. };
  494. export type FieldTag = {
  495. key: FieldKey;
  496. name: FieldKey;
  497. };
  498. export const FIELD_TAGS = Object.freeze(
  499. Object.fromEntries(Object.keys(FIELDS).map(item => [item, {key: item, name: item}]))
  500. );
  501. // Allows for a less strict field key definition in cases we are returning custom strings as fields
  502. export type LooseFieldKey = FieldKey | string | '';
  503. export enum WebVital {
  504. FP = 'measurements.fp',
  505. FCP = 'measurements.fcp',
  506. LCP = 'measurements.lcp',
  507. FID = 'measurements.fid',
  508. CLS = 'measurements.cls',
  509. TTFB = 'measurements.ttfb',
  510. RequestTime = 'measurements.ttfb.requesttime',
  511. }
  512. const MEASUREMENTS: Readonly<Record<WebVital, ColumnType>> = {
  513. [WebVital.FP]: 'duration',
  514. [WebVital.FCP]: 'duration',
  515. [WebVital.LCP]: 'duration',
  516. [WebVital.FID]: 'duration',
  517. [WebVital.CLS]: 'number',
  518. [WebVital.TTFB]: 'duration',
  519. [WebVital.RequestTime]: 'duration',
  520. };
  521. // This list contains fields/functions that are available with performance-view feature.
  522. export const TRACING_FIELDS = [
  523. 'avg',
  524. 'sum',
  525. 'transaction.duration',
  526. 'transaction.op',
  527. 'transaction.status',
  528. 'p50',
  529. 'p75',
  530. 'p95',
  531. 'p99',
  532. 'p100',
  533. 'percentile',
  534. 'failure_rate',
  535. 'apdex',
  536. 'count_miserable',
  537. 'user_misery',
  538. 'eps',
  539. 'epm',
  540. ...Object.keys(MEASUREMENTS),
  541. ];
  542. export const MEASUREMENT_PATTERN = /^measurements\.([a-zA-Z0-9-_.]+)$/;
  543. export const SPAN_OP_BREAKDOWN_PATTERN = /^spans\.([a-zA-Z0-9-_.]+)$/;
  544. export function isMeasurement(field: string): boolean {
  545. const results = field.match(MEASUREMENT_PATTERN);
  546. return !!results;
  547. }
  548. export function measurementType(field: string) {
  549. if (MEASUREMENTS.hasOwnProperty(field)) {
  550. return MEASUREMENTS[field];
  551. }
  552. return 'number';
  553. }
  554. export function getMeasurementSlug(field: string): string | null {
  555. const results = field.match(MEASUREMENT_PATTERN);
  556. if (results && results.length >= 2) {
  557. return results[1];
  558. }
  559. return null;
  560. }
  561. const AGGREGATE_PATTERN = /^([^\(]+)\((.*?)(?:\s*,\s*(.*))?\)$/;
  562. export function getAggregateArg(field: string): string | null {
  563. const results = field.match(AGGREGATE_PATTERN);
  564. if (results && results.length >= 3) {
  565. return results[2];
  566. }
  567. return null;
  568. }
  569. export function generateAggregateFields(
  570. organization: LightWeightOrganization,
  571. eventFields: readonly Field[] | Field[],
  572. excludeFields: readonly string[] = []
  573. ): Field[] {
  574. const functions = Object.keys(AGGREGATIONS);
  575. const fields = Object.values(eventFields).map(field => field.field);
  576. functions.forEach(func => {
  577. const parameters = AGGREGATIONS[func].parameters.map(param => {
  578. const generator = AGGREGATIONS[func].generateDefaultValue;
  579. if (typeof generator === 'undefined') {
  580. return param;
  581. }
  582. return {
  583. ...param,
  584. defaultValue: generator({parameter: param, organization}),
  585. };
  586. });
  587. if (parameters.every(param => typeof param.defaultValue !== 'undefined')) {
  588. const newField = `${func}(${parameters
  589. .map(param => param.defaultValue)
  590. .join(',')})`;
  591. if (fields.indexOf(newField) === -1 && excludeFields.indexOf(newField) === -1) {
  592. fields.push(newField);
  593. }
  594. }
  595. });
  596. return fields.map(field => ({field})) as Field[];
  597. }
  598. export function explodeFieldString(field: string): Column {
  599. const results = field.match(AGGREGATE_PATTERN);
  600. if (results && results.length >= 3) {
  601. return {
  602. kind: 'function',
  603. function: [
  604. results[1] as AggregationKey,
  605. results[2],
  606. results[3] as AggregationRefinement,
  607. ],
  608. };
  609. }
  610. return {kind: 'field', field};
  611. }
  612. export function generateFieldAsString(value: QueryFieldValue): string {
  613. if (value.kind === 'field') {
  614. return value.field;
  615. }
  616. const aggregation = value.function[0];
  617. const parameters = value.function.slice(1).filter(i => i);
  618. return `${aggregation}(${parameters.join(',')})`;
  619. }
  620. export function explodeField(field: Field): Column {
  621. const results = explodeFieldString(field.field);
  622. return results;
  623. }
  624. /**
  625. * Get the alias that the API results will have for a given aggregate function name
  626. */
  627. export function getAggregateAlias(field: string): string {
  628. if (!field.match(AGGREGATE_PATTERN)) {
  629. return field;
  630. }
  631. return field
  632. .replace(AGGREGATE_PATTERN, '$1_$2_$3')
  633. .replace(/[^\w]/g, '_')
  634. .replace(/^_+/g, '')
  635. .replace(/_+$/, '');
  636. }
  637. /**
  638. * Check if a field name looks like an aggregate function or known aggregate alias.
  639. */
  640. export function isAggregateField(field: string): boolean {
  641. return field.match(AGGREGATE_PATTERN) !== null;
  642. }
  643. /**
  644. * Convert a function string into type it will output.
  645. * This is useful when you need to format values in tooltips,
  646. * or in series markers.
  647. */
  648. export function aggregateOutputType(field: string): AggregationOutputType {
  649. const matches = AGGREGATE_PATTERN.exec(field);
  650. if (!matches) {
  651. return 'number';
  652. }
  653. const outputType = aggregateFunctionOutputType(matches[1], matches[2]);
  654. if (outputType === null) {
  655. return 'number';
  656. }
  657. return outputType;
  658. }
  659. /**
  660. * Converts a function string and its first argument into its output type.
  661. * - If the function has a fixed output type, that will be the result.
  662. * - If the function does not define an output type, the output type will be equal to
  663. * the type of its first argument.
  664. * - If the function has an optional first argument, and it was not defined, make sure
  665. * to use the default argument as the first argument.
  666. * - If the type could not be determined, return null.
  667. */
  668. export function aggregateFunctionOutputType(
  669. funcName: string,
  670. firstArg: string | undefined
  671. ): AggregationOutputType | null {
  672. const aggregate = AGGREGATIONS[ALIASES[funcName] || funcName];
  673. // Attempt to use the function's outputType.
  674. if (aggregate?.outputType) {
  675. return aggregate.outputType;
  676. }
  677. // If the first argument is undefined and it is not required,
  678. // then we attempt to get the default value.
  679. if (!firstArg && aggregate?.parameters?.[0]) {
  680. if (aggregate.parameters[0].required === false) {
  681. firstArg = aggregate.parameters[0].defaultValue;
  682. }
  683. }
  684. // If the function is an inherit type it will have a field as
  685. // the first parameter and we can use that to get the type.
  686. if (firstArg && FIELDS.hasOwnProperty(firstArg)) {
  687. return FIELDS[firstArg];
  688. } else if (firstArg && isMeasurement(firstArg)) {
  689. return measurementType(firstArg);
  690. } else if (firstArg && isSpanOperationBreakdownField(firstArg)) {
  691. return 'duration';
  692. }
  693. return null;
  694. }
  695. /**
  696. * Get the multi-series chart type for an aggregate function.
  697. */
  698. export function aggregateMultiPlotType(field: string): PlotType {
  699. const matches = AGGREGATE_PATTERN.exec(field);
  700. // Handle invalid data.
  701. if (!matches) {
  702. return 'area';
  703. }
  704. const funcName = matches[1];
  705. if (!AGGREGATIONS.hasOwnProperty(funcName)) {
  706. return 'area';
  707. }
  708. return AGGREGATIONS[funcName].multiPlotType;
  709. }
  710. function validateForNumericAggregate(
  711. validColumnTypes: ColumnType[]
  712. ): ValidateColumnValueFunction {
  713. return function ({name, dataType}: {name: string; dataType: ColumnType}): boolean {
  714. // these built-in columns cannot be applied to numeric aggregates such as percentile(...)
  715. if (
  716. [
  717. FieldKey.DEVICE_BATTERY_LEVEL,
  718. FieldKey.STACK_COLNO,
  719. FieldKey.STACK_LINENO,
  720. FieldKey.STACK_STACK_LEVEL,
  721. ].includes(name as FieldKey)
  722. ) {
  723. return false;
  724. }
  725. return validColumnTypes.includes(dataType);
  726. };
  727. }
  728. function validateAllowedColumns(validColumns: string[]): ValidateColumnValueFunction {
  729. return function ({name}): boolean {
  730. return validColumns.includes(name);
  731. };
  732. }
  733. const alignedTypes: ColumnValueType[] = ['number', 'duration', 'integer', 'percentage'];
  734. export function fieldAlignment(
  735. columnName: string,
  736. columnType?: undefined | ColumnValueType,
  737. metadata?: Record<string, ColumnValueType>
  738. ): Alignments {
  739. let align: Alignments = 'left';
  740. if (columnType) {
  741. align = alignedTypes.includes(columnType) ? 'right' : 'left';
  742. }
  743. if (columnType === undefined || columnType === 'never') {
  744. // fallback to align the column based on the table metadata
  745. const maybeType = metadata ? metadata[getAggregateAlias(columnName)] : undefined;
  746. if (maybeType !== undefined && alignedTypes.includes(maybeType)) {
  747. align = 'right';
  748. }
  749. }
  750. return align;
  751. }
  752. /**
  753. * Match on types that are legal to show on a timeseries chart.
  754. */
  755. export function isLegalYAxisType(match: ColumnType) {
  756. return ['number', 'integer', 'duration', 'percentage'].includes(match);
  757. }
  758. export function isSpanOperationBreakdownField(field: string) {
  759. return field.startsWith('spans.');
  760. }
  761. export function getSpanOperationName(field: string): string | null {
  762. const results = field.match(SPAN_OP_BREAKDOWN_PATTERN);
  763. if (results && results.length >= 2) {
  764. return results[1];
  765. }
  766. return null;
  767. }